Shared media improvements

This commit is contained in:
Ali 2021-10-24 22:27:23 +04:00
parent 05e57b1923
commit d28747cb00
9 changed files with 415 additions and 184 deletions

View File

@ -18,10 +18,11 @@ public protocol ContextActionNodeProtocol: ASDisplayNode {
final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
private var presentationData: PresentationData private var presentationData: PresentationData
private var action: ContextMenuActionItem private(set) var action: ContextMenuActionItem
private let getController: () -> ContextControllerProtocol? private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void private let actionSelected: (ContextMenuActionResult) -> Void
private let requestLayout: () -> Void private let requestLayout: () -> Void
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
@ -40,12 +41,13 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
return true return true
} }
init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void) { init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) {
self.presentationData = presentationData self.presentationData = presentationData
self.action = action self.action = action
self.getController = getController self.getController = getController
self.actionSelected = actionSelected self.actionSelected = actionSelected
self.requestLayout = requestLayout self.requestLayout = requestLayout
self.requestUpdateAction = requestUpdateAction
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
@ -316,6 +318,37 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
@objc private func buttonPressed() { @objc private func buttonPressed() {
self.performAction() self.performAction()
} }
func updateAction(item: ContextMenuActionItem) {
self.action = item
let textColor: UIColor
switch self.action.textColor {
case .primary:
textColor = self.presentationData.theme.contextMenu.primaryColor
case .destructive:
textColor = self.presentationData.theme.contextMenu.destructiveColor
case .disabled:
textColor = self.presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
}
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
let titleFont: UIFont
switch self.action.textFont {
case .regular:
titleFont = textFont
case let .custom(customFont):
titleFont = customFont
}
self.textNode.attributedText = NSAttributedString(string: self.action.text, font: titleFont, textColor: textColor)
if self.action.iconSource == nil {
self.iconNode.image = self.action.icon(self.presentationData.theme)
}
self.requestLayout()
}
func performAction() { func performAction() {
guard let controller = self.getController() else { guard let controller = self.getController() else {
@ -326,38 +359,11 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
dismissWithResult: { [weak self] result in dismissWithResult: { [weak self] result in
self?.actionSelected(result) self?.actionSelected(result)
}, },
updateAction: { [weak self] updatedAction in updateAction: { [weak self] id, updatedAction in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.action = updatedAction strongSelf.requestUpdateAction(id, updatedAction)
let textColor: UIColor
switch strongSelf.action.textColor {
case .primary:
textColor = strongSelf.presentationData.theme.contextMenu.primaryColor
case .destructive:
textColor = strongSelf.presentationData.theme.contextMenu.destructiveColor
case .disabled:
textColor = strongSelf.presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
}
let textFont = Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize)
let titleFont: UIFont
switch strongSelf.action.textFont {
case .regular:
titleFont = textFont
case let .custom(customFont):
titleFont = customFont
}
strongSelf.textNode.attributedText = NSAttributedString(string: strongSelf.action.text, font: titleFont, textColor: textColor)
if strongSelf.action.iconSource == nil {
strongSelf.iconNode.image = strongSelf.action.icon(strongSelf.presentationData.theme)
}
strongSelf.requestLayout()
} }
)) ))
} }

View File

@ -78,12 +78,16 @@ private final class InnerActionsContainerNode: ASDisplayNode {
self.containerNode.clipsToBounds = true self.containerNode.clipsToBounds = true
self.containerNode.cornerRadius = 14.0 self.containerNode.cornerRadius = 14.0
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
var requestUpdateAction: ((AnyHashable, ContextMenuActionItem) -> Void)?
var itemNodes: [ContextItemNode] = [] var itemNodes: [ContextItemNode] = []
for i in 0 ..< items.count { for i in 0 ..< items.count {
switch items[i] { switch items[i] {
case let .action(action): case let .action(action):
itemNodes.append(.action(ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout))) itemNodes.append(.action(ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, requestUpdateAction: { id, action in
requestUpdateAction?(id, action)
})))
if i != items.count - 1 { if i != items.count - 1 {
switch items[i + 1] { switch items[i + 1] {
case .action, .custom: case .action, .custom:
@ -116,7 +120,24 @@ private final class InnerActionsContainerNode: ASDisplayNode {
self.itemNodes = itemNodes self.itemNodes = itemNodes
super.init() super.init()
requestUpdateAction = { [weak self] id, action in
guard let strongSelf = self else {
return
}
loop: for itemNode in strongSelf.itemNodes {
switch itemNode {
case let .action(contextActionNode):
if contextActionNode.action.id == id {
contextActionNode.updateAction(item: action)
break loop
}
default:
break
}
}
}
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.itemNodes.forEach({ itemNode in self.itemNodes.forEach({ itemNode in

View File

@ -71,15 +71,16 @@ public final class ContextMenuActionItem {
public final class Action { public final class Action {
public let controller: ContextControllerProtocol public let controller: ContextControllerProtocol
public let dismissWithResult: (ContextMenuActionResult) -> Void public let dismissWithResult: (ContextMenuActionResult) -> Void
public let updateAction: (ContextMenuActionItem) -> Void public let updateAction: (AnyHashable, ContextMenuActionItem) -> Void
init(controller: ContextControllerProtocol, dismissWithResult: @escaping (ContextMenuActionResult) -> Void, updateAction: @escaping (ContextMenuActionItem) -> Void) { init(controller: ContextControllerProtocol, dismissWithResult: @escaping (ContextMenuActionResult) -> Void, updateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) {
self.controller = controller self.controller = controller
self.dismissWithResult = dismissWithResult self.dismissWithResult = dismissWithResult
self.updateAction = updateAction self.updateAction = updateAction
} }
} }
public let id: AnyHashable?
public let text: String public let text: String
public let textColor: ContextMenuActionItemTextColor public let textColor: ContextMenuActionItemTextColor
public let textFont: ContextMenuActionItemFont public let textFont: ContextMenuActionItemFont
@ -90,6 +91,7 @@ public final class ContextMenuActionItem {
public let action: ((Action) -> Void)? public let action: ((Action) -> Void)?
convenience public init( convenience public init(
id: AnyHashable? = nil,
text: String, text: String,
textColor: ContextMenuActionItemTextColor = .primary, textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
@ -100,6 +102,7 @@ public final class ContextMenuActionItem {
action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)? action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?
) { ) {
self.init( self.init(
id: id,
text: text, text: text,
textColor: textColor, textColor: textColor,
textLayout: textLayout, textLayout: textLayout,
@ -116,6 +119,7 @@ public final class ContextMenuActionItem {
} }
public init( public init(
id: AnyHashable? = nil,
text: String, text: String,
textColor: ContextMenuActionItemTextColor = .primary, textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
@ -125,6 +129,7 @@ public final class ContextMenuActionItem {
iconSource: ContextMenuActionItemIconSource? = nil, iconSource: ContextMenuActionItemIconSource? = nil,
action: ((Action) -> Void)? action: ((Action) -> Void)?
) { ) {
self.id = id
self.text = text self.text = text
self.textColor = textColor self.textColor = textColor
self.textFont = textFont self.textFont = textFont

View File

@ -88,7 +88,15 @@ public final class ContextControllerSourceNode: ASDisplayNode {
} }
} }
contextGesture.activated = { [weak self] gesture, location in contextGesture.activated = { [weak self] gesture, location in
if let activated = self?.activated { guard let strongSelf = self else {
gesture.cancel()
return
}
if let customActivationProgress = strongSelf.customActivationProgress {
customActivationProgress(0.0, .ended(0.0))
}
if let activated = strongSelf.activated {
activated(gesture, location) activated(gesture, location)
} else { } else {
gesture.cancel() gesture.cancel()

View File

@ -342,9 +342,11 @@ public final class SparseItemGrid: ASDisplayNode {
return self.displayLayer.frame return self.displayLayer.frame
} set(value) { } set(value) {
if let layer = self.layer { if let layer = self.layer {
layer.frame = value layer.bounds = CGRect(origin: CGPoint(), size: value.size)
layer.position = value.center
} else if let view = self.view { } else if let view = self.view {
view.frame = value view.bounds = CGRect(origin: CGPoint(), size: value.size)
view.center = value.center
} else { } else {
preconditionFailure() preconditionFailure()
} }
@ -564,6 +566,22 @@ public final class SparseItemGrid: ASDisplayNode {
} }
} }
func visualItem(at point: CGPoint) -> SparseItemGridDisplayItem? {
guard let items = self.items, !items.items.isEmpty else {
return nil
}
let localPoint = self.scrollView.convert(point, from: self.view)
for (_, visibleItem) in self.visibleItems {
if visibleItem.frame.contains(localPoint) {
return visibleItem
}
}
return nil
}
func item(at point: CGPoint) -> Item? { func item(at point: CGPoint) -> Item? {
guard let items = self.items, !items.items.isEmpty else { guard let items = self.items, !items.items.isEmpty else {
return nil return nil
@ -658,6 +676,10 @@ public final class SparseItemGrid: ASDisplayNode {
} }
} }
func stopScrolling() {
self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false)
}
private func updateVisibleItems(resetScrolling: Bool, synchronous: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) { private func updateVisibleItems(resetScrolling: Bool, synchronous: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) {
guard let layout = self.layout, let items = self.items else { guard let layout = self.layout, let items = self.items else {
return return
@ -1219,7 +1241,7 @@ public final class SparseItemGrid: ASDisplayNode {
self.scrollingArea.isHidden = lockScrollingAtTop self.scrollingArea.isHidden = lockScrollingAtTop
self.tapRecognizer?.isEnabled = fixedItemHeight == nil self.tapRecognizer?.isEnabled = fixedItemHeight == nil
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil self.pinchRecognizer?.isEnabled = fixedItemHeight == nil && !lockScrollingAtTop
if self.currentViewport == nil { if self.currentViewport == nil {
let currentViewport = Viewport(zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in let currentViewport = Viewport(zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
@ -1359,6 +1381,13 @@ public final class SparseItemGrid: ASDisplayNode {
return self.view.convert(currentViewport.frameForItem(layer: layer), from: currentViewport.view) return self.view.convert(currentViewport.frameForItem(layer: layer), from: currentViewport.view)
} }
public func item(at point: CGPoint) -> SparseItemGridDisplayItem? {
guard let currentViewport = self.currentViewport else {
return nil
}
return currentViewport.visualItem(at: point)
}
public func scrollToItem(at index: Int) { public func scrollToItem(at index: Int) {
guard let currentViewport = self.currentViewport else { guard let currentViewport = self.currentViewport else {
return return
@ -1380,4 +1409,15 @@ public final class SparseItemGrid: ASDisplayNode {
public func updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip) { public func updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip) {
self.scrollingArea.displayTooltip = tooltip self.scrollingArea.displayTooltip = tooltip
} }
public func cancelGestures() {
self.tapRecognizer?.state = .cancelled
self.pinchRecognizer?.state = .cancelled
}
public func hideScrollingArea() {
self.currentViewport?.stopScrolling()
self.scrollingArea.hideScroller()
}
} }

View File

@ -164,16 +164,28 @@ public final class TooltipComponent: Component {
private var icon: ComponentHostView<Empty>? private var icon: ComponentHostView<Empty>?
private let content: ComponentHostView<Empty> private let content: ComponentHostView<Empty>
private let regularMaskImage: UIImage
private let invertedMaskImage: UIImage
init() { init() {
self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.backgroundViewMask = UIImageView() self.backgroundViewMask = UIImageView()
self.backgroundViewMask.image = generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in self.regularMaskImage = generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.black.cgColor) context.setFillColor(UIColor.black.cgColor)
let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ") let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ")
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 34) })!.stretchableImage(withLeftCapWidth: 16, topCapHeight: 33)
self.invertedMaskImage = generateImage(CGSize(width: 42.0, height: 42.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.black.cgColor)
let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ")
})!.stretchableImage(withLeftCapWidth: 16, topCapHeight: 9)
self.backgroundViewMask.image = self.regularMaskImage
self.content = ComponentHostView<Empty>() self.content = ComponentHostView<Empty>()
@ -235,14 +247,21 @@ public final class TooltipComponent: Component {
if contentRect.minX < 0.0 { if contentRect.minX < 0.0 {
contentRect.origin.x = component.arrowLocation.maxX contentRect.origin.x = component.arrowLocation.maxX
} }
if contentRect.minY < 0.0 {
contentRect.origin.y = component.arrowLocation.minY - contentRect.height let maskedBackgroundFrame: CGRect
if contentRect.maxY > availableSize.height {
self.backgroundViewMask.image = self.invertedMaskImage
contentRect.origin.y = component.arrowLocation.minY - contentRect.height - 4.0
maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0 + 3.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 8.0))
self.backgroundViewMask.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: maskedBackgroundFrame.size)
} else {
self.backgroundViewMask.image = self.regularMaskImage
maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 4.0))
self.backgroundViewMask.frame = CGRect(origin: CGPoint(), size: maskedBackgroundFrame.size)
} }
let maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 4.0))
self.backgroundView.frame = maskedBackgroundFrame self.backgroundView.frame = maskedBackgroundFrame
self.backgroundViewMask.frame = CGRect(origin: CGPoint(), size: maskedBackgroundFrame.size)
if let iconSize = iconSize, let icon = self.icon { if let iconSize = iconSize, let icon = self.icon {
transition.setFrame(view: icon, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - iconSize.height) / 2.0)), size: iconSize)) transition.setFrame(view: icon, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - iconSize.height) / 2.0)), size: iconSize))
@ -800,7 +819,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top
let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom
let scrollIndicatorHeight = max(44.0, ceil(scrollIndicatorHeightFraction * containerSize.height)) let scrollIndicatorHeight: CGFloat = 44.0
let indicatorPositionFraction = min(1.0, max(0.0, contentOffset / (contentHeight - containerSize.height))) let indicatorPositionFraction = min(1.0, max(0.0, contentOffset / (contentHeight - containerSize.height)))
@ -964,4 +983,12 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
return nil return nil
} }
public func hideScroller() {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 0.0)
transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 0.0)
self.dismissLineTooltip()
}
} }

View File

@ -732,10 +732,12 @@ final class CachedPeerInvitationImporters: Codable {
self.dates = dates self.dates = dates
var abouts: [PeerId: String] = [:] var abouts: [PeerId: String] = [:]
let aboutsArray = try container.decode([DictionaryPair].self, forKey: "abouts") let aboutsArray = try container.decodeIfPresent([DictionaryPair].self, forKey: "abouts")
for aboutPair in aboutsArray { if let aboutsArray = aboutsArray {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(aboutPair.key)) for aboutPair in aboutsArray {
abouts[peerId] = aboutPair.value let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(aboutPair.key))
abouts[peerId] = aboutPair.value
}
} }
self.abouts = abouts self.abouts = abouts

View File

@ -1055,7 +1055,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
} }
var duration: Int32? var duration: Int32?
if layer.bounds.width > 80.0, let file = selectedMedia as? TelegramMediaFile { if layer.bounds.width > 80.0, let file = selectedMedia as? TelegramMediaFile, !file.isAnimated {
duration = file.duration duration = file.duration
} }
layer.updateDuration(duration: duration) layer.updateDuration(duration: duration)
@ -1172,6 +1172,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
weak var parentController: ViewController? weak var parentController: ViewController?
private let contextGestureContainerNode: ContextControllerSourceNode
private let itemGrid: SparseItemGrid private let itemGrid: SparseItemGrid
private let itemGridBinding: SparseItemGridBindingImpl private let itemGridBinding: SparseItemGridBindingImpl
private let directMediaImageCache: DirectMediaImageCache private let directMediaImageCache: DirectMediaImageCache
@ -1221,6 +1222,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private let stateTag: MessageTags private let stateTag: MessageTags
private var storedStateDisposable: Disposable? private var storedStateDisposable: Disposable?
private weak var currentGestureItem: SparseItemGridDisplayItem?
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType) { init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType) {
self.context = context self.context = context
@ -1230,6 +1233,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.contentTypePromise = ValuePromise<ContentType>(contentType) self.contentTypePromise = ValuePromise<ContentType>(contentType)
self.stateTag = tagMaskForType(contentType) self.stateTag = tagMaskForType(contentType)
self.contextGestureContainerNode = ContextControllerSourceNode()
self.itemGrid = SparseItemGrid() self.itemGrid = SparseItemGrid()
self.directMediaImageCache = DirectMediaImageCache(account: context.account) self.directMediaImageCache = DirectMediaImageCache(account: context.account)
@ -1398,7 +1402,94 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
) )
self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
self.addSubnode(self.itemGrid) self.contextGestureContainerNode.isGestureEnabled = !useListItems
self.contextGestureContainerNode.addSubnode(self.itemGrid)
self.addSubnode(self.contextGestureContainerNode)
self.contextGestureContainerNode.shouldBegin = { [weak self] point in
guard let strongSelf = self else {
return false
}
guard let item = strongSelf.itemGrid.item(at: point) else {
return false
}
strongSelf.currentGestureItem = item
return true
}
self.contextGestureContainerNode.customActivationProgress = { [weak self] progress, update in
guard let strongSelf = self, let currentGestureItem = strongSelf.currentGestureItem else {
return
}
guard let itemLayer = currentGestureItem.layer else {
return
}
let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size)
let scaleSide = itemLayer.bounds.width
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX
let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY
let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
switch update {
case .update:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
itemLayer.transform = sublayerTransform
case .begin:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
itemLayer.transform = sublayerTransform
case .ended:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
let previousTransform = itemLayer.transform
itemLayer.transform = sublayerTransform
itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "transform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
}
}
self.contextGestureContainerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let currentGestureItem = strongSelf.currentGestureItem else {
return
}
strongSelf.currentGestureItem = nil
guard let itemLayer = currentGestureItem.layer as? ItemLayer else {
return
}
guard let message = itemLayer.item?.message else {
return
}
let rect = strongSelf.itemGrid.frameForItem(layer: itemLayer)
let proxyNode = ASDisplayNode()
proxyNode.frame = rect
proxyNode.contents = itemLayer.contents
proxyNode.isHidden = true
strongSelf.addSubnode(proxyNode)
let escapeNotification = EscapeNotification {
proxyNode.removeFromSupernode()
}
Queue.mainQueue().after(1.0, {
escapeNotification.keep()
})
strongSelf.chatControllerInteraction.openMessageContextActions(message, proxyNode, proxyNode.bounds, gesture)
strongSelf.itemGrid.cancelGestures()
}
self.storedStateDisposable = (visualMediaStoredState(postbox: context.account.postbox, peerId: peerId, messageTag: self.stateTag) self.storedStateDisposable = (visualMediaStoredState(postbox: context.account.postbox, peerId: peerId, messageTag: self.stateTag)
|> deliverOnMainQueue).start(next: { [weak self] value in |> deliverOnMainQueue).start(next: { [weak self] value in
@ -1509,7 +1600,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} else if photoCount != 0 { } else if photoCount != 0 {
return PeerInfoStatusData(text: "\(photoCount) photos", isActivity: false) return PeerInfoStatusData(text: "\(photoCount) photos", isActivity: false)
} else if videoCount != 0 { } else if videoCount != 0 {
return PeerInfoStatusData(text: "\(photoCount) videos", isActivity: false) return PeerInfoStatusData(text: "\(videoCount) videos", isActivity: false)
} else { } else {
return nil return nil
} }
@ -1607,6 +1698,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.contentType = contentType self.contentType = contentType
self.contentTypePromise.set(contentType) self.contentTypePromise.set(contentType)
self.itemGrid.hideScrollingArea()
self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType)) self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType))
self.isRequestingView = false self.isRequestingView = false
self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true) self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true)
@ -1836,6 +1929,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
if let items = self.items { if let items = self.items {
let fixedItemHeight: CGFloat? let fixedItemHeight: CGFloat?

View File

@ -6022,141 +6022,168 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) { private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) {
guard let controller = self.controller else { let summaryTags: [MessageTags] = [.photo, .video]
return let peerId = self.peerId
} let _ = (context.account.postbox.combinedView(keys: summaryTags.map { tag in
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoVisualMediaPaneNode else { return PostboxViewKey.historyTagSummaryView(tag: tag, peerId: peerId, namespace: Namespaces.Message.Cloud)
return })
} |> deliverOnMainQueue).start(next: { [weak self] views in
var items: [ContextMenuItem] = []
//TODO:localize
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
var canZoom: Bool = true
if !"".isEmpty {
canZoom = false
}
return ContextMenuActionItem(text: isZoomIn ? "Zoom In" : "Zoom Out", textColor: canZoom ? .primary : .disabled, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
}, action: canZoom ? { action in
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
return
}
pane.updateZoomLevel(level: zoomLevel)
if let recurseGenerateAction = recurseGenerateAction {
action.updateAction(recurseGenerateAction(isZoomIn))
}
} : nil)
}
recurseGenerateAction = { isZoomIn in
return generateAction(isZoomIn)
}
items.append(.action(generateAction(true)))
items.append(.action(generateAction(false)))
items.append(.action(ContextMenuActionItem(text: "Show Calendar", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
self?.openMediaCalendar()
})))
items.append(.separator)
let showPhotos: Bool
switch pane.contentType {
case .photo, .photoOrVideo:
showPhotos = true
default:
showPhotos = false
}
let showVideos: Bool
switch pane.contentType {
case .video, .photoOrVideo:
showVideos = true
default:
showVideos = false
}
items.append(.action(ContextMenuActionItem(text: "Show Photos", icon: { theme in
if !showPhotos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .video
case .photo:
updatedContentType = .photo
case .video:
updatedContentType = .photoOrVideo
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
items.append(.action(ContextMenuActionItem(text: "Show Videos", icon: { theme in
if !showVideos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .photo
case .photo:
updatedContentType = .photoOrVideo
case .video:
updatedContentType = .video
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(items: items)), gesture: nil)
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let strongSelf = self else { guard let strongSelf = self else {
return .ignore return
} }
let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil) var mediaCount: [MessageTags: Int32] = [:]
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else { for tag in summaryTags {
if let view = views.views[PostboxViewKey.historyTagSummaryView(tag: tag, peerId: peerId, namespace: Namespaces.Message.Cloud)] as? MessageHistoryTagSummaryView {
mediaCount[tag] = view.count ?? 0
} else {
mediaCount[tag] = 0
}
}
let photoCount: Int32 = mediaCount[.photo] ?? 0
let videoCount: Int32 = mediaCount[.video] ?? 0
guard let controller = strongSelf.controller else {
return
}
guard let pane = strongSelf.paneContainerNode.currentPane?.node as? PeerInfoVisualMediaPaneNode else {
return
}
var items: [ContextMenuItem] = []
//TODO:localize
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
let canZoom: Bool = nextZoomLevel != nil
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? "Zoom In" : "Zoom Out", textColor: canZoom ? .primary : .disabled, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
}, action: canZoom ? { action in
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
return
}
pane.updateZoomLevel(level: zoomLevel)
if let recurseGenerateAction = recurseGenerateAction {
action.updateAction(0, recurseGenerateAction(true))
action.updateAction(1, recurseGenerateAction(false))
}
} : nil)
}
recurseGenerateAction = { isZoomIn in
return generateAction(isZoomIn)
}
items.append(.action(generateAction(true)))
items.append(.action(generateAction(false)))
items.append(.action(ContextMenuActionItem(text: "Show Calendar", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
a(.default)
self?.openMediaCalendar()
})))
if photoCount != 0 && videoCount != 0 {
items.append(.separator)
let showPhotos: Bool
switch pane.contentType {
case .photo, .photoOrVideo:
showPhotos = true
default:
showPhotos = false
}
let showVideos: Bool
switch pane.contentType {
case .video, .photoOrVideo:
showVideos = true
default:
showVideos = false
}
items.append(.action(ContextMenuActionItem(text: "Show Photos", icon: { theme in
if !showPhotos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .video
case .photo:
updatedContentType = .photo
case .video:
updatedContentType = .photoOrVideo
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
items.append(.action(ContextMenuActionItem(text: "Show Videos", icon: { theme in
if !showVideos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .photo
case .photo:
updatedContentType = .photoOrVideo
case .video:
updatedContentType = .video
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(items: items)), gesture: nil)
contextController.passthroughTouchEvent = { sourceView, point in
guard let strongSelf = self else {
return .ignore
}
let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil)
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else {
return .dismiss(consume: true)
}
var testView: UIView? = localResult
while true {
if let testViewValue = testView {
if testViewValue.asyncdisplaykit_node is PeerInfoVisualMediaPaneNode {
return .dismiss(consume: false)
} else {
testView = testViewValue.superview
}
} else {
break
}
}
return .dismiss(consume: true) return .dismiss(consume: true)
} }
strongSelf.mediaGalleryContextMenu = contextController
var testView: UIView? = localResult controller.presentInGlobalOverlay(contextController)
while true { })
if let testViewValue = testView {
if testViewValue.asyncdisplaykit_node is PeerInfoVisualMediaPaneNode {
return .dismiss(consume: false)
} else {
testView = testViewValue.superview
}
} else {
break
}
}
return .dismiss(consume: true)
}
self.mediaGalleryContextMenu = contextController
controller.presentInGlobalOverlay(contextController)
} }
private func openMediaCalendar() { private func openMediaCalendar() {