Context UI improvements

This commit is contained in:
Ali 2022-03-25 21:04:52 +04:00
parent f1a6e9fdaa
commit e1b4e15da8
9 changed files with 824 additions and 182 deletions

View File

@ -1129,6 +1129,13 @@
"Time.MediumDate" = "%1$@ at %2$@";
"MuteFor.Minutes_1" = "Mute for 1 minute";
"MuteFor.Minutes_2" = "Mute for 2 minutes";
"MuteFor.Minutes_3_10" = "Mute for %@ minutes";
"MuteFor.Minutes_any" = "Mute for %@ minutes";
"MuteFor.Minutes_many" = "Mute for %@ minutes";
"MuteFor.Minutes_0" = "Mute for %@ minutes";
"MuteFor.Hours_1" = "Mute for 1 hour";
"MuteFor.Hours_2" = "Mute for 2 hours";
"MuteFor.Hours_3_10" = "Mute for %@ hours";

View File

@ -51,6 +51,7 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
self.requestUpdateAction = requestUpdateAction
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isAccessibilityElement = false
@ -78,6 +79,8 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
switch action.textFont {
case .regular:
titleFont = textFont
case .small:
titleFont = smallTextFont
case let .custom(customFont):
titleFont = customFont
}
@ -102,6 +105,9 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
statusNode.attributedText = NSAttributedString(string: value, font: subtitleFont, textColor: presentationData.theme.contextMenu.secondaryColor)
statusNode.maximumNumberOfLines = 1
self.statusNode = statusNode
case .multiline:
self.textNode.maximumNumberOfLines = 0
self.statusNode = nil
}
self.iconNode = ASImageNode()
@ -290,10 +296,13 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
}
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
let titleFont: UIFont
switch self.action.textFont {
case .regular:
titleFont = textFont
case .small:
titleFont = smallTextFont
case let .custom(customFont):
titleFont = customFont
}
@ -334,10 +343,13 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
}
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
let titleFont: UIFont
switch self.action.textFont {
case .regular:
titleFont = textFont
case .small:
titleFont = smallTextFont
case let .custom(customFont):
titleFont = customFont
}

View File

@ -27,6 +27,7 @@ public enum ContextMenuActionItemTextLayout {
case singleLine
case twoLinesMax
case secondLineWithValue(String)
case multiline
}
public enum ContextMenuActionItemTextColor {
@ -43,6 +44,7 @@ public enum ContextMenuActionResult {
public enum ContextMenuActionItemFont {
case regular
case small
case custom(UIFont)
}
@ -542,7 +544,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private func initializeContent() {
switch self.source {
case let .reference(source):
let transitionInfo = source.transitionInfo()
/*let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo {
let referenceView = transitionInfo.referenceView
self.contentContainerNode.contentNode = .reference(view: referenceView)
@ -554,7 +556,40 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
projectedFrame.origin.y += transitionInfo.insets.top
projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
}
}*/
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
return self?.getController()
},
requestUpdate: { [weak self] transition in
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(
layout: validLayout,
transition: transition,
previousActionsContainerNode: nil
)
}
},
requestDismiss: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.dismissedForCancel?()
strongSelf.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let strongSelf = self else {
return
}
strongSelf.animateOut(result: result, completion: completion)
},
source: .reference(source)
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
case let .extracted(source):
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
@ -585,7 +620,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
strongSelf.animateOut(result: result, completion: completion)
},
source: source
source: .extracted(source)
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)

View File

@ -164,12 +164,17 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
case let .secondLineWithValue(subtitleValue):
self.titleLabelNode.maximumNumberOfLines = 1
subtitle = subtitleValue
case .multiline:
self.titleLabelNode.maximumNumberOfLines = 0
}
let titleFont: UIFont
switch self.item.textFont {
case let .custom(font):
titleFont = font
case .small:
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
titleFont = smallTextFont
case .regular:
titleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
}
@ -479,7 +484,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
let itemNodeLayout = item.node.update(
presentationData: presentationData,
constrainedSize: constrainedSize
constrainedSize: CGSize(width: standardWidth, height: constrainedSize.height)
)
itemNodeLayouts.append(itemNodeLayout)
combinedSize.width = max(combinedSize.width, itemNodeLayout.minSize.width)
@ -677,7 +682,15 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> Co
}
final class ContextControllerActionsStackNode: ASDisplayNode {
enum Presentation {
case modal
case inline
}
final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let backgroundNode: NavigationBackgroundNode
let parentShadowNode: ASImageNode
var requestUpdate: ((ContainedViewLayoutTransition) -> Void)?
var requestPop: (() -> Void)?
var transitionFraction: CGFloat = 0.0
@ -691,8 +704,14 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
}
override init() {
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
self.parentShadowNode = ASImageNode()
self.parentShadowNode.image = UIImage(bundleImageName: "Components/Context Menu/Shadow")?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
super.init()
self.addSubnode(self.backgroundNode)
self.clipsToBounds = true
self.cornerRadius = 14.0
@ -748,7 +767,16 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
}
}
func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) {
func update(presentationData: PresentationData, presentation: Presentation, size: CGSize, transition: ContainedViewLayoutTransition) {
switch presentation {
case .modal:
self.backgroundNode.updateColor(color: presentationData.theme.contextMenu.backgroundColor, enableBlur: false, forceKeepBlur: false, transition: transition)
self.parentShadowNode.isHidden = true
case .inline:
self.backgroundNode.updateColor(color: presentationData.theme.contextMenu.backgroundColor, enableBlur: true, forceKeepBlur: true, transition: transition)
self.parentShadowNode.isHidden = false
}
self.backgroundNode.update(size: size, transition: transition)
}
}
@ -895,6 +923,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
super.init()
self.addSubnode(self.navigationContainer.parentShadowNode)
self.addSubnode(self.navigationContainer)
self.navigationContainer.requestUpdate = { [weak self] transition in
@ -999,12 +1028,11 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
func update(
presentationData: PresentationData,
constrainedSize: CGSize,
presentation: Presentation,
transition: ContainedViewLayoutTransition
) -> CGSize {
let tipSpacing: CGFloat = 10.0
self.navigationContainer.backgroundColor = presentationData.theme.contextMenu.backgroundColor
let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty
struct ItemLayout {
@ -1083,7 +1111,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight)))
transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true)
self.navigationContainer.update(presentationData: presentationData, size: navigationContainerFrame.size, transition: transition)
self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition)
let navigationContainerShadowFrame = navigationContainerFrame.insetBy(dx: -30.0, dy: -30.0)
transition.updateFrame(node: self.navigationContainer.parentShadowNode, frame: navigationContainerShadowFrame, beginWithCurrentState: true)
for i in 0 ..< self.itemContainers.count {
let xOffset: CGFloat

View File

@ -9,6 +9,11 @@ import SwiftSignalKit
import ReactionSelectionNode
final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode, UIScrollViewDelegate {
enum ContentSource {
case reference(ContextReferenceContentSource)
case extracted(ContextExtractedContentSource)
}
private final class ContentNode: ASDisplayNode {
let offsetContainerNode: ASDisplayNode
let containingNode: ContextExtractedContentContainingNode
@ -59,7 +64,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestDismiss: (ContextMenuActionResult) -> Void
private let requestAnimateOut: (ContextMenuActionResult, @escaping () -> Void) -> Void
private let source: ContextExtractedContentSource
private let source: ContentSource
private let backgroundNode: NavigationBackgroundNode
private let dismissTapNode: ASDisplayNode
@ -83,7 +88,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void,
source: ContextExtractedContentSource
source: ContentSource
) {
self.getController = getController
self.requestUpdate = requestUpdate
@ -159,7 +164,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
if !self.source.ignoreContentTouches, let contentNode = self.contentNode {
if case let .extracted(source) = self.source, !source.ignoreContentTouches, let contentNode = self.contentNode {
let contentPoint = self.view.convert(point, to: contentNode.containingNode.contentNode.view)
if let result = contentNode.containingNode.contentNode.customHitTest?(contentPoint) {
return result
@ -230,12 +235,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
stateTransition: ContextControllerPresentationNodeStateTransition?
) {
let contentActionsSpacing: CGFloat = 7.0
let actionsEdgeInset: CGFloat = 12.0
let actionsEdgeInset: CGFloat
let actionsSideInset: CGFloat = 6.0
let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0
let bottomInset: CGFloat = 10.0
let contentNode: ContentNode
let contentNode: ContentNode?
var contentTransition = transition
if self.strings !== presentationData.strings {
@ -244,12 +249,24 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu
}
self.backgroundNode.updateColor(
color: presentationData.theme.contextMenu.dimColor,
enableBlur: true,
forceKeepBlur: true,
transition: .immediate
)
switch self.source {
case .reference:
self.backgroundNode.updateColor(
color: .clear,
enableBlur: false,
forceKeepBlur: false,
transition: .immediate
)
actionsEdgeInset = 16.0
case .extracted:
self.backgroundNode.updateColor(
color: presentationData.theme.contextMenu.dimColor,
enableBlur: true,
forceKeepBlur: true,
transition: .immediate
)
actionsEdgeInset = 12.0
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true)
self.backgroundNode.update(size: layout.size, transition: transition)
@ -262,14 +279,20 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
if let current = self.contentNode {
contentNode = current
} else {
guard let takeInfo = self.source.takeView() else {
return
switch self.source {
case .reference:
contentNode = nil
case let .extracted(source):
guard let takeInfo = source.takeView() else {
return
}
let contentNodeValue = ContentNode(containingNode: takeInfo.contentContainingNode)
contentNodeValue.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace
self.scrollNode.insertSubnode(contentNodeValue, aboveSubnode: self.actionsStackNode)
self.contentNode = contentNodeValue
contentNode = contentNodeValue
contentTransition = .immediate
}
contentNode = ContentNode(containingNode: takeInfo.contentContainingNode)
contentNode.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace
self.scrollNode.insertSubnode(contentNode, aboveSubnode: self.actionsStackNode)
self.contentNode = contentNode
contentTransition = .immediate
}
var animateReactionsIn = false
@ -298,30 +321,64 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
removedReactionContextNode = reactionContextNode
}
switch stateTransition {
case .animateIn, .animateOut:
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
case .none:
if contentNode.storedGlobalFrame == nil {
if let contentNode = contentNode {
switch stateTransition {
case .animateIn, .animateOut:
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
case .none:
if contentNode.storedGlobalFrame == nil {
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
}
}
}
let contentParentGlobalFrame = convertFrame(contentNode.containingNode.bounds, from: contentNode.containingNode.view, to: self.view)
let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size)
var contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size)
if case .animateOut = stateTransition {
contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height
let contentParentGlobalFrame: CGRect
var contentRect: CGRect
switch self.source {
case let .reference(reference):
if let transitionInfo = reference.transitionInfo() {
contentRect = convertFrame(transitionInfo.referenceView.bounds, from: transitionInfo.referenceView, to: self.view).insetBy(dx: -2.0, dy: 0.0)
contentRect.size.width += 5.0
contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minX), size: CGSize(width: layout.size.width, height: contentRect.height))
} else {
return
}
case .extracted:
if let contentNode = contentNode {
contentParentGlobalFrame = convertFrame(contentNode.containingNode.bounds, from: contentNode.containingNode.view, to: self.view)
let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size)
contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size)
if case .animateOut = stateTransition {
contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height
}
} else {
return
}
}
let keepInPlace: Bool
let centerActionsHorizontally: Bool
switch self.source {
case .reference:
keepInPlace = true
centerActionsHorizontally = false
case let .extracted(source):
keepInPlace = source.keepInPlace
centerActionsHorizontally = source.centerActionsHorizontally
}
var defaultScrollY: CGFloat = 0.0
if self.animatingOutState == nil {
contentNode.update(
presentationData: presentationData,
size: contentNode.containingNode.bounds.size,
transition: contentTransition
)
if let contentNode = contentNode {
contentNode.update(
presentationData: presentationData,
size: contentNode.containingNode.bounds.size,
transition: contentTransition
)
}
let actionsConstrainedHeight: CGFloat
if let actionsPositionLock = self.actionsStackNode.topPositionLock {
@ -330,9 +387,18 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
actionsConstrainedHeight = layout.size.height - contentTopInset - contentRect.height - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom
}
let actionsStackPresentation: ContextControllerActionsStackNode.Presentation
switch self.source {
case .reference:
actionsStackPresentation = .inline
case .extracted:
actionsStackPresentation = .modal
}
let actionsSize = self.actionsStackNode.update(
presentationData: presentationData,
constrainedSize: CGSize(width: layout.size.width, height: actionsConstrainedHeight),
presentation: actionsStackPresentation,
transition: transition
)
@ -340,7 +406,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
} else {
if let topPositionLock = self.actionsStackNode.topPositionLock {
contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height
} else if self.source.keepInPlace {
} else if keepInPlace {
} else {
if contentRect.minY < contentTopInset {
contentRect.origin.y = contentTopInset
@ -375,10 +441,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true)
var actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize)
if self.source.keepInPlace {
if keepInPlace, case .extracted = self.source {
actionsFrame.origin.y = contentRect.minY - contentActionsSpacing - actionsFrame.height
}
if self.source.centerActionsHorizontally {
if centerActionsHorizontally {
actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0)
if actionsFrame.maxX > layout.size.width - actionsEdgeInset {
actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width
@ -390,7 +457,18 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
if contentRect.midX < layout.size.width / 2.0 {
actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.minX + actionsSideInset - 4.0
} else {
actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0
switch self.source {
case .reference:
actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0)
if actionsFrame.maxX > layout.size.width - actionsEdgeInset {
actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width
}
if actionsFrame.minX < actionsEdgeInset {
actionsFrame.origin.x = actionsEdgeInset
}
case .extracted:
actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0
}
}
if actionsFrame.maxX > layout.size.width - actionsEdgeInset {
actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width
@ -401,7 +479,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame, beginWithCurrentState: true)
contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size), beginWithCurrentState: true)
if let contentNode = contentNode {
contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size), beginWithCurrentState: true)
}
let contentHeight: CGFloat
if self.actionsStackNode.topPositionLock != nil {
@ -438,7 +518,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
switch stateTransition {
case .animateIn:
contentNode.takeContainingNode()
if let contentNode = contentNode {
contentNode.takeContainingNode()
}
let duration: Double = 0.42
let springDamping: CGFloat = 104.0
@ -447,25 +529,32 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2)
let animationInContentDistance: CGFloat
let currentContentScreenFrame: CGRect
if let contentNode = contentNode {
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2)
}
currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
animationInContentDistance = currentContentLocalFrame.maxY - currentContentScreenFrame.maxY
contentNode.layer.animateSpring(
from: -animationInContentDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.y",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
} else {
animationInContentDistance = 0.0
currentContentScreenFrame = contentRect
}
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
let animationInContentDistance = currentContentLocalFrame.maxY - currentContentScreenFrame.maxY
contentNode.layer.animateSpring(
from: -animationInContentDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.y",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
self.actionsStackNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05)
self.actionsStackNode.layer.animateSpring(
from: 0.01 as NSNumber,
@ -481,15 +570,23 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let actionsSize = self.actionsStackNode.bounds.size
var actionsPositionDeltaXDistance: CGFloat = 0.0
if self.source.centerActionsHorizontally {
if centerActionsHorizontally {
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX
}
let actionsVerticalTransitionDirection: CGFloat
if contentNode.frame.minY < self.actionsStackNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
if let contentNode = contentNode {
if contentNode.frame.minY < self.actionsStackNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
} else {
actionsVerticalTransitionDirection = 1.0
}
} else {
actionsVerticalTransitionDirection = 1.0
if contentRect.minY < self.actionsStackNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
} else {
actionsVerticalTransitionDirection = 1.0
}
}
let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing
self.actionsStackNode.layer.animateSpring(
@ -520,55 +617,22 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.actionsStackNode.animateIn()
contentNode.containingNode.isExtractedToContextPreview = true
contentNode.containingNode.isExtractedToContextPreviewUpdated?(true)
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(true, transition)
contentNode.containingNode.layoutUpdated = { [weak self] _, animation in
guard let strongSelf = self, let _ = strongSelf.contentNode else {
return
}
if let contentNode = contentNode {
contentNode.containingNode.isExtractedToContextPreview = true
contentNode.containingNode.isExtractedToContextPreviewUpdated?(true)
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(true, transition)
if let _ = strongSelf.animatingOutState {
/*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view)
if animatingOutState.currentContentScreenFrame != updatedContentScreenFrame {
let offset = CGPoint(
x: updatedContentScreenFrame.minX - animatingOutState.currentContentScreenFrame.minX,
y: updatedContentScreenFrame.minY - animatingOutState.currentContentScreenFrame.minY
)
let _ = offset
//animation.animator.updatePosition(layer: contentNode.layer, position: contentNode.position.offsetBy(dx: offset.x, dy: offset.y), completion: nil)
animatingOutState.currentContentScreenFrame = updatedContentScreenFrame
}*/
} else {
strongSelf.requestUpdate(animation.transition)
contentNode.containingNode.layoutUpdated = { [weak self] _, animation in
guard let strongSelf = self, let _ = strongSelf.contentNode else {
return
}
/*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view)
if let storedGlobalFrame = contentNode.storedGlobalFrame {
let offset = CGPoint(
x: updatedContentScreenFrame.minX - storedGlobalFrame.minX,
y: updatedContentScreenFrame.maxY - storedGlobalFrame.maxY
)
if !offset.x.isZero || !offset.y.isZero {
//print("contentNode.frame = \(contentNode.frame)")
//animation.animator.updateBounds(layer: contentNode.layer, bounds: contentNode.layer.bounds.offsetBy(dx: -offset.x, dy: -offset.y), completion: nil)
}
//animatingOutState.currentContentScreenFrame = updatedContentScreenFrame
}*/
if let _ = strongSelf.animatingOutState {
} else {
strongSelf.requestUpdate(animation.transition)
}
}
}
/*
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
public var layoutUpdated: ((CGSize) -> Void)?
public var updateDistractionFreeMode: ((Bool) -> Void)?
public var requestDismiss: (() -> Void)*/
case let .animateOut(result, completion):
let duration: Double
let timingFunction: String
@ -587,15 +651,33 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
let putBackInfo = self.source.putBack()
let currentContentScreenFrame: CGRect
if let putBackInfo = putBackInfo {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
switch self.source {
case let .reference(source):
if let putBackInfo = source.transitionInfo() {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
currentContentScreenFrame = convertFrame(putBackInfo.referenceView.bounds, from: putBackInfo.referenceView, to: self.view)
} else {
return
}
case let .extracted(source):
let putBackInfo = source.putBack()
if let putBackInfo = putBackInfo {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
}
if let contentNode = contentNode {
currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
} else {
return
}
}
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
self.animatingOutState = AnimatingOutState(
currentContentScreenFrame: currentContentScreenFrame
)
@ -609,41 +691,53 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
animationInContentDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY
case .dismissWithoutContent:
animationInContentDistance = 0.0
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
if let contentNode = contentNode {
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
}
}
let actionsVerticalTransitionDirection: CGFloat
if contentNode.frame.minY < self.actionsStackNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
if let contentNode = contentNode {
if contentNode.frame.minY < self.actionsStackNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
} else {
actionsVerticalTransitionDirection = 1.0
}
} else {
actionsVerticalTransitionDirection = 1.0
if contentRect.minY < self.actionsStackNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
} else {
actionsVerticalTransitionDirection = 1.0
}
}
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition)
contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance)
let reactionContextNodeIsAnimatingOut = self.reactionContextNodeIsAnimatingOut
contentNode.offsetContainerNode.layer.animate(
from: animationInContentDistance as NSNumber,
to: 0.0 as NSNumber,
keyPath: "position.y",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
additive: true,
completion: { [weak self] _ in
Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, {
contentNode.containingNode.isExtractedToContextPreview = false
contentNode.containingNode.isExtractedToContextPreviewUpdated?(false)
if let strongSelf = self, let contentNode = strongSelf.contentNode {
contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode)
}
completion()
})
}
)
if let contentNode = contentNode {
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition)
contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance)
let reactionContextNodeIsAnimatingOut = self.reactionContextNodeIsAnimatingOut
contentNode.offsetContainerNode.layer.animate(
from: animationInContentDistance as NSNumber,
to: 0.0 as NSNumber,
keyPath: "position.y",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
additive: true,
completion: { [weak self] _ in
Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, {
contentNode.containingNode.isExtractedToContextPreview = false
contentNode.containingNode.isExtractedToContextPreviewUpdated?(false)
if let strongSelf = self, let contentNode = strongSelf.contentNode {
contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode)
}
completion()
})
}
)
}
self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.actionsStackNode.layer.animate(
@ -659,7 +753,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let actionsSize = self.actionsStackNode.bounds.size
var actionsPositionDeltaXDistance: CGFloat = 0.0
if self.source.centerActionsHorizontally {
if centerActionsHorizontally {
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX
}
let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing

View File

@ -119,7 +119,9 @@ public func shortTimeIntervalString(strings: PresentationStrings, value: Int32)
}
public func muteForIntervalString(strings: PresentationStrings, value: Int32) -> String {
if value < 60 * 60 * 24 {
if value < 60 * 60 {
return strings.MuteFor_Minutes(max(1, value / (60)))
} else if value < 60 * 60 * 24 {
return strings.MuteFor_Hours(max(1, value / (60 * 60)))
} else {
return strings.MuteFor_Days(max(1, value / (60 * 60 * 24)))

View File

@ -270,6 +270,23 @@ public func stringForRelativeTimestamp(strings: PresentationStrings, relativeTim
}
}
public func stringForPreciseRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 {
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
} else {
return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)), \(stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat))"
}
}
public func stringForRelativeLiveLocationTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
let difference = timestamp - relativeTimestamp
if difference < 60 {

View File

@ -9,12 +9,19 @@ import AccountContext
import SolidRoundedButtonNode
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
enum ChatTimerScreenStyle {
case `default`
case media
}
enum ChatTimerScreenMode {
case sendTimer
case autoremove
case mute
}
final class ChatTimerScreen: ViewController {
private var controllerNode: ChatTimerScreenNode {
return self.displayNode as! ChatTimerScreenNode
@ -25,6 +32,7 @@ final class ChatTimerScreen: ViewController {
private let context: AccountContext
private let peerId: PeerId
private let style: ChatTimerScreenStyle
private let mode: ChatTimerScreenMode
private let currentTime: Int32?
private let dismissByTapOutside: Bool
private let completion: (Int32) -> Void
@ -32,10 +40,11 @@ final class ChatTimerScreen: ViewController {
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, style: ChatTimerScreenStyle, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
self.context = context
self.peerId = peerId
self.style = style
self.mode = mode
self.currentTime = currentTime
self.dismissByTapOutside = dismissByTapOutside
self.completion = completion
@ -68,7 +77,7 @@ final class ChatTimerScreen: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = ChatTimerScreenNode(context: self.context, presentationData: presentationData, style: self.style, currentTime: self.currentTime, dismissByTapOutside: self.dismissByTapOutside)
self.displayNode = ChatTimerScreenNode(context: self.context, presentationData: presentationData, style: self.style, mode: self.mode, currentTime: self.currentTime, dismissByTapOutside: self.dismissByTapOutside)
self.controllerNode.completion = { [weak self] time in
guard let strongSelf = self else {
return
@ -108,7 +117,45 @@ final class ChatTimerScreen: ViewController {
}
}
private class TimerPickerView: UIPickerView {
private protocol TimerPickerView: UIView {
}
private class TimerCustomPickerView: UIPickerView, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
}
private class TimerDatePickerView: UIDatePicker, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
@ -213,6 +260,7 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi
private let controllerStyle: ChatTimerScreenStyle
private var presentationData: PresentationData
private let dismissByTapOutside: Bool
private let mode: ChatTimerScreenMode
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
@ -233,11 +281,12 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData, style: ChatTimerScreenStyle, currentTime: Int32?, dismissByTapOutside: Bool) {
init(context: AccountContext, presentationData: PresentationData, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode, currentTime: Int32?, dismissByTapOutside: Bool) {
self.context = context
self.controllerStyle = style
self.presentationData = presentationData
self.dismissByTapOutside = dismissByTapOutside
self.mode = mode
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
@ -278,7 +327,17 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi
self.contentBackgroundNode = ASDisplayNode()
self.contentBackgroundNode.backgroundColor = backgroundColor
let title = self.presentationData.strings.Conversation_Timer_Title
let title: String
switch self.mode {
case .sendTimer:
title = self.presentationData.strings.Conversation_Timer_Title
case .autoremove:
//TODO:localize
title = "Auto-Delete After..."
case .mute:
//TODO:localize
title = "Mute Until..."
}
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
@ -315,7 +374,19 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi
self.doneButton.pressed = { [weak self] in
if let strongSelf = self, let pickerView = strongSelf.pickerView {
strongSelf.doneButton.isUserInteractionEnabled = false
strongSelf.completion?(timerValues[pickerView.selectedRow(inComponent: 0)])
if let pickerView = pickerView as? TimerCustomPickerView {
strongSelf.completion?(timerValues[pickerView.selectedRow(inComponent: 0)])
} else if let pickerView = pickerView as? TimerDatePickerView {
switch strongSelf.mode {
case .autoremove:
strongSelf.completion?(Int32(pickerView.countDownDuration))
case .mute:
let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
strongSelf.completion?(timeInterval)
default:
break
}
}
}
}
@ -327,13 +398,51 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi
pickerView.removeFromSuperview()
}
let pickerView = TimerPickerView()
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.dataSource = self
pickerView.delegate = self
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
switch self.mode {
case .sendTimer:
let pickerView = TimerCustomPickerView()
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.dataSource = self
pickerView.delegate = self
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
case .autoremove:
let pickerView = TimerDatePickerView()
pickerView.locale = localeWithStrings(self.presentationData.strings)
pickerView.datePickerMode = .countDownTimer
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.addTarget(self, action: #selector(self.dataPickerChanged), for: .valueChanged)
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
case .mute:
let pickerView = TimerDatePickerView()
pickerView.locale = localeWithStrings(self.presentationData.strings)
pickerView.datePickerMode = .dateAndTime
pickerView.minimumDate = Date()
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.addTarget(self, action: #selector(self.dataPickerChanged), for: .valueChanged)
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
}
}
@objc private func dataPickerChanged() {
guard let _ = self.pickerView as? TimerDatePickerView else {
return
}
if let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
@ -498,6 +607,33 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0
switch self.mode {
case .sendTimer:
break
case .autoremove:
if let pickerView = self.pickerView as? TimerDatePickerView, pickerView.countDownDuration > 0.0 {
self.doneButton.title = "Auto-Delete after \(timeIntervalString(strings: self.presentationData.strings, value: Int32(pickerView.countDownDuration)))"
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
case .mute:
if let pickerView = self.pickerView as? TimerDatePickerView {
let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
if timeInterval > 0 {
let timeString = stringForPreciseRelativeTimestamp(strings: self.presentationData.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
//TODO:localize
self.doneButton.title = "Mute until \(timeString)"
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
}
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight))

View File

@ -3531,31 +3531,105 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
var items: [ContextMenuItem] = []
let muteValues: [(Int32, String)] = [
(1 * 60 * 60, "Chat/Context Menu/Mute2h"),
(2 * 24 * 60 * 60, "Chat/Context Menu/Mute2d"),
(Int32.max, "Chat/Context Menu/Muted")
]
for (delay, iconName) in muteValues {
let title: String
if delay == Int32.max {
title = self.presentationData.strings.MuteFor_Forever
} else {
title = muteForIntervalString(strings: self.presentationData.strings, value: delay)
items.append(.action(ContextMenuActionItem(text: "Mute for...", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Mute2d"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
guard let strongSelf = self else {
return
}
var subItems: [ContextMenuItem] = []
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c.popItems()
})))
subItems.append(.separator)
let presetValues: [Int32] = [
1 * 60 * 60,
8 * 60 * 60,
1 * 24 * 60 * 60,
7 * 24 * 60 * 60
]
for value in presetValues {
subItems.append(.action(ContextMenuActionItem(text: muteForIntervalString(strings: strongSelf.presentationData.strings, value: value), icon: { _ in
return nil
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, muteInterval: value).start()
})))
}
items.append(.action(ContextMenuActionItem(text: title, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: iconName), color: theme.contextMenu.primaryColor)
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Mute until...", icon: { _ in
return nil
}, action: { _, f in
f(.dismissWithoutContent)
f(.default)
let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: self.peerId, muteInterval: delay).start()
self?.openCustomMute()
})))
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
})))
items.append(.separator)
var isSoundEnabled = true
if let notificationSettings = self.data?.notificationSettings {
switch notificationSettings.messageSound {
case .none:
isSoundEnabled = false
default:
break
}
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Sound On", icon: { theme in
if !isSoundEnabled {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let strongSelf = self else {
return
}
if isSoundEnabled {
return
}
let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, sound: .default).start()
})))
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Sound Off", icon: { theme in
if isSoundEnabled {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let strongSelf = self else {
return
}
if !isSoundEnabled {
return
}
let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, sound: .none).start()
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.PeerInfo_CustomizeNotifications, icon: { theme in
items.append(.action(ContextMenuActionItem(text: "Customize", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
@ -3601,6 +3675,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
controller.push(exceptionController)
})))
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Mute Forever", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Muted"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.default)
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, muteInterval: Int32.max).start()
})))
self.view.endEditing(true)
if let sourceNode = self.headerNode.buttonNodes[.mute]?.referenceNode {
@ -3646,13 +3733,135 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
if canChangeColors {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ChangeColors, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
}, action: { _, f in
f(.dismissWithoutContent)
self?.openChatForThemeChange()
})))
}
var currentAutoremoveTimeout: Int32?
if let cachedData = data.cachedData as? CachedUserData {
switch cachedData.autoremoveTimeout {
case let .known(value):
currentAutoremoveTimeout = value?.peerValue
case .unknown:
break
}
} else if let cachedData = data.cachedData as? CachedGroupData {
switch cachedData.autoremoveTimeout {
case let .known(value):
currentAutoremoveTimeout = value?.peerValue
case .unknown:
break
}
} else if let cachedData = data.cachedData as? CachedChannelData {
switch cachedData.autoremoveTimeout {
case let .known(value):
currentAutoremoveTimeout = value?.peerValue
case .unknown:
break
}
}
var canSetupAutoremoveTimeout = false
if let secretChat = peer as? TelegramSecretChat {
currentAutoremoveTimeout = secretChat.messageAutoremoveTimeout
canSetupAutoremoveTimeout = true
} else if let group = peer as? TelegramGroup {
if case .creator = group.role {
canSetupAutoremoveTimeout = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canDeleteMessages) {
canSetupAutoremoveTimeout = true
}
}
} else if let user = peer as? TelegramUser {
if user.id != strongSelf.context.account.peerId && user.botInfo == nil {
canSetupAutoremoveTimeout = true
}
} else if let channel = peer as? TelegramChannel {
if channel.hasPermission(.deleteAllMessages) {
canSetupAutoremoveTimeout = true
}
}
if canSetupAutoremoveTimeout {
//TODO:localize
let strings = strongSelf.presentationData.strings
items.append(.action(ContextMenuActionItem(text: currentAutoremoveTimeout == nil ? "Enable Auto-Delete" : "Adjust Auto-Delete", icon: { theme in
if let currentAutoremoveTimeout = currentAutoremoveTimeout {
let text = NSAttributedString(string: shortTimeIntervalString(strings: strings, value: currentAutoremoveTimeout), font: Font.regular(14.0), textColor: theme.contextMenu.primaryColor)
let bounds = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
return generateImage(bounds.size.integralFloor, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
text.draw(in: bounds)
UIGraphicsPopContext()
})
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor)
}
}, action: { [weak self] c, _ in
var subItems: [ContextMenuItem] = []
subItems.append(.action(ContextMenuActionItem(text: strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c.popItems()
})))
subItems.append(.separator)
let presetValues: [Int32] = [
60 * 60,
24 * 60 * 60,
7 * 24 * 60 * 60,
31 * 24 * 60 * 60
]
if let _ = currentAutoremoveTimeout {
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Disable", icon: { _ in
return nil
}, action: { _, f in
f(.default)
self?.setAutoremove(timeInterval: nil)
})))
}
for value in presetValues {
subItems.append(.action(ContextMenuActionItem(text: timeIntervalString(strings: strings, value: value), icon: { _ in
return nil
}, action: { _, f in
f(.default)
self?.setAutoremove(timeInterval: value)
})))
}
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Other...", icon: { _ in
return nil
}, action: { _, f in
f(.default)
self?.openAutoremove(currentValue: currentAutoremoveTimeout)
})))
subItems.append(.separator)
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Automatically delete messages sent in this chat after a certain period of time.", textLayout: .multiline, textFont: .small, icon: { _ in
return nil
}, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?)))
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
})))
items.append(.separator)
}
if filteredButtons.contains(.call) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ButtonCall, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor)
@ -3771,11 +3980,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self?.openStartSecretChat()
})))
}
/*if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser {
items.append(.action(ContextMenuActionItem(text: "", icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openStartSecretChat()
})))
}*/
if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) {
if data.isContact {
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_BlockUser, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.primaryColor)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_BlockUser, textColor: .destructive, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
@ -3931,6 +4153,92 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
}
private func openAutoremove(currentValue: Int32?) {
/*let controller = peerAutoremoveSetupScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, completion: { [weak self] updatedValue in
if case let .updated(value) = updatedValue {
guard let strongSelf = self else {
return
}
var isOn: Bool = true
var text: String?
if let myValue = value.value {
text = strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: myValue))").string
} else {
isOn = false
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
}
if let text = text {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
}
}
})
self.controller?.view.endEditing(true)
self.controller?.push(controller)*/
let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: strongSelf.peerId, timeout: value == 0 ? nil : value)
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
var isOn: Bool = true
var text: String?
if value != 0 {
text = strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: value))").string
} else {
isOn = false
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
}
if let text = text {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
})
self.controller?.view.endEditing(true)
self.controller?.present(controller, in: .window(.root))
}
private func openCustomMute() {
let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak self] value in
guard let strongSelf = self else {
return
}
if value <= 0 {
let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, muteInterval: nil).start()
} else {
let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, muteInterval: value).start()
}
})
self.controller?.view.endEditing(true)
self.controller?.present(controller, in: .window(.root))
}
private func setAutoremove(timeInterval: Int32?) {
let _ = (self.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: self.peerId, timeout: timeInterval)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
var isOn: Bool = true
var text: String?
if let myValue = timeInterval {
text = strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: myValue))").string
} else {
isOn = false
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
}
if let text = text {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
}
private func openStartSecretChat() {
let peerId = self.peerId
let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, PeerId?) in