mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Context UI improvements
This commit is contained in:
parent
f1a6e9fdaa
commit
e1b4e15da8
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user