Voice Chat Improvements

This commit is contained in:
Ilya Laktyushin 2021-03-04 18:59:06 +04:00
parent 1fbceb81d1
commit 74d1896e04
50 changed files with 4848 additions and 4462 deletions

View File

@ -5824,6 +5824,8 @@ Sorry for the inconvenience.";
"VoiceChat.StopRecordingTitle" = "Stop Recording?";
"VoiceChat.StopRecordingStop" = "Stop";
"VoiceChat.RecordingSaved" = "Audio stream saved to Saved Messages.";
"VoiceChat.StatusMutedForYou" = "muted for you";
"VoiceChat.StatusMutedYou" = "put you on mute";
@ -6187,14 +6189,13 @@ Sorry for the inconvenience.";
"VoiceChat.DisplayAs" = "Display Me As...";
"VoiceChat.DisplayAsInfo" = "Choose whether you want to be displayed as your personal account or as your channel.";
"VoiceChat.DisplayAsSuccess" = "Members of this voice chat will now see your as **%@**.";
"VoiceChat.PersonalAccount" = "personal account";
"VoiceChat.EditTitle" = "Edit Voice Chat Title";
"VoiceChat.EditPermissions" = "Edit Permissions";
"VoiceChat.OpenChannel" = "Open Channel";
"VoiceChat.DisplayAsSuccess" = "Members of this voice chat will now see your as **%@**.";
"VoiceChat.EditTitleTitle" = "Voice Chat Title";
"VoiceChat.EditTitleText" = "Edit a title of this voice chat.";
"VoiceChat.EditTitleSuccess" = "Voice chat title changed to **%@**.";

View File

@ -223,7 +223,7 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
transition.updateFrameAdditive(node: statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: textSize))
transition.updateFrameAdditive(node: statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: statusSize))
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)

View File

@ -212,7 +212,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
var feedbackTap: (() -> Void)?
var blurBackground = true
if case let .extracted(extractedSource) = source, !extractedSource.blurBackground {
if case .reference = source {
blurBackground = false
} else if case let .extracted(extractedSource) = source, !extractedSource.blurBackground {
blurBackground = false
}
self.blurBackground = blurBackground
@ -441,7 +443,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}))
switch source {
case .extracted:
case .reference, .extracted:
self.contentReady.set(.single(true))
case let .controller(source):
self.contentReady.set(source.controller.ready.get())
@ -478,6 +480,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private func initializeContent() {
switch self.source {
case let .reference(source):
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo {
let referenceNode = transitionInfo.referenceNode
self.contentContainerNode.contentNode = .reference(node: referenceNode)
self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
let projectedFrame = convertFrame(referenceNode.view.bounds, from: referenceNode.view, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
}
case let .extracted(source):
let takenViewInfo = source.takeView()
@ -553,6 +564,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.hapticFeedback.impact()
switch self.source {
case .reference:
break
case .extracted:
if let contentAreaInScreenSpace = self.contentAreaInScreenSpace, let maybeContentNode = self.contentContainerNode.contentNode, case .extracted = maybeContentNode {
var updatedContentAreaInScreenSpace = contentAreaInScreenSpace
@ -619,6 +632,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let contentNode = self.contentContainerNode.contentNode {
switch contentNode {
case let .reference(referenceNode):
let springDuration: Double = 0.42 * animationDurationFactor
let springDamping: CGFloat = 104.0
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor)
self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame {
let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view)
let localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview)
self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y)
self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
}
case let .extracted(extracted, keepInPlace):
let springDuration: Double = 0.42 * animationDurationFactor
let springDamping: CGFloat = 104.0
@ -701,6 +730,74 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
var result = initialResult
switch self.source {
case let .reference(source):
guard let maybeContentNode = self.contentContainerNode.contentNode, case let .reference(referenceNode) = maybeContentNode else {
return
}
let transitionInfo = source.transitionInfo()
if transitionInfo == nil {
result = .dismissWithoutContent
}
switch result {
case let .custom(value):
switch value {
case let .animated(duration, curve):
transitionDuration = duration
transitionCurve = curve
default:
break
}
default:
break
}
self.isUserInteractionEnabled = false
self.isAnimatingOut = true
self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false)
var completedEffect = false
var completedContentNode = false
var completedActionsNode = false
if let transitionInfo = transitionInfo, let parentSupernode = referenceNode.supernode {
self.originalProjectedContentViewFrame = (convertFrame(referenceNode.frame, from: parentSupernode.view, to: self.view), convertFrame(referenceNode.bounds, from: referenceNode.view, to: self.view))
var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
updatedContentAreaInScreenSpace.origin.x = 0.0
updatedContentAreaInScreenSpace.size.width = self.bounds.width
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
}
if !self.dimNode.isHidden {
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false)
} else {
self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false)
}
self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in
completion()
})
self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
let animateOutToItem: Bool
switch result {
case .default, .custom:
animateOutToItem = true
case .dismissWithoutContent:
animateOutToItem = false
}
if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame {
let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view)
let localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview)
self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true)
}
case let .extracted(source):
guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode else {
return
@ -1111,7 +1208,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
switch layout.metrics.widthClass {
case .compact:
if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground {
if case .reference = self.source {
} else if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground {
} else if self.effectView.superview == nil {
self.view.insertSubview(self.effectView, at: 0)
if #available(iOS 10.0, *) {
@ -1126,7 +1224,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.dimNode.isHidden = false
self.withoutBlurDimNode.isHidden = true
case .regular:
if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground {
if case .reference = self.source {
} else if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground {
} else if self.effectView.superview != nil {
self.effectView.removeFromSuperview()
self.withoutBlurDimNode.alpha = 1.0
@ -1147,6 +1246,83 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let contentNode = self.contentContainerNode.contentNode {
switch contentNode {
case let .reference(referenceNode):
let contentActionsSpacing: CGFloat = 16.0
if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame {
let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition)
self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize)
let contentSize = originalProjectedContentViewFrame.1.size
self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition)
let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height)
let originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin)
let preferredActionsX = originalProjectedContentViewFrame.1.minX
var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: actionsSize)
let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX
let originalContentY = originalProjectedContentViewFrame.1.minY
var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size)
let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0)
if originalContentFrame.minY < topEdge {
let requiredOffset = topEdge - originalContentFrame.minY
let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY)
let offset = min(requiredOffset, availableOffset)
originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset)
originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset)
}
var contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset)
var overflowOffset: CGFloat
var contentContainerFrame: CGRect
// if keepInPlace {
overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset)
let contentParentNode = referenceNode
contentContainerFrame = originalContentFrame
if !overflowOffset.isZero {
let offsetDelta = contentParentNode.frame.height + 4.0
overflowOffset += offsetDelta
overflowOffset = min(0.0, overflowOffset)
originalActionsFrame.origin.x -= contentParentNode.frame.width + 14.0
originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x)
//originalActionsFrame.origin.y += contentParentNode.contentRect.height
if originalActionsFrame.minX < contentContainerFrame.minX {
contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset)
}
originalActionsFrame.origin.y += offsetDelta
if originalActionsFrame.maxY < originalContentFrame.maxY {
originalActionsFrame.origin.y += contentParentNode.frame.height
originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset)
}
contentHeight -= offsetDelta
}
// } else {
// overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset)
// contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY)
// }
let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight)
if self.scrollNode.view.contentSize != scrollContentSize {
self.scrollNode.view.contentSize = scrollContentSize
}
self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset))
if isInitialLayout {
let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
if overflowOffset < 0.0 {
transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY)
}
}
}
case let .extracted(contentParentNode, keepInPlace):
let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0
if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame {
@ -1198,7 +1374,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if keepInPlace {
overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset)
contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -contentParentNode.contentRect.minY)
if keepInPlace && !overflowOffset.isZero {
if !overflowOffset.isZero {
let offsetDelta = contentParentNode.contentRect.height + 4.0
overflowOffset += offsetDelta
overflowOffset = min(0.0, overflowOffset)
@ -1452,6 +1628,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
let mappedPoint = self.view.convert(point, to: self.scrollNode.view)
if let maybeContentNode = self.contentContainerNode.contentNode {
switch maybeContentNode {
case .reference:
break
case let .extracted(contentParentNode, _):
if case let .extracted(source) = self.source {
if !source.ignoreContentTouches {
@ -1493,6 +1671,20 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
public final class ContextControllerReferenceViewInfo {
public let referenceNode: ContextReferenceContentNode
public let contentAreaInScreenSpace: CGRect
public init(referenceNode: ContextReferenceContentNode, contentAreaInScreenSpace: CGRect) {
self.referenceNode = referenceNode
self.contentAreaInScreenSpace = contentAreaInScreenSpace
}
}
public protocol ContextReferenceContentSource: class {
func transitionInfo() -> ContextControllerReferenceViewInfo?
}
public final class ContextControllerTakeViewInfo {
public let contentContainingNode: ContextExtractedContentContainingNode
public let contentAreaInScreenSpace: CGRect
@ -1503,16 +1695,6 @@ public final class ContextControllerTakeViewInfo {
}
}
public final class ContextControllerTakeControllerInfo {
public let contentAreaInScreenSpace: CGRect
public let sourceNode: () -> (ASDisplayNode, CGRect)?
public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (ASDisplayNode, CGRect)?) {
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.sourceNode = sourceNode
}
}
public final class ContextControllerPutBackViewInfo {
public let contentAreaInScreenSpace: CGRect
@ -1537,6 +1719,16 @@ public extension ContextExtractedContentSource {
}
}
public final class ContextControllerTakeControllerInfo {
public let contentAreaInScreenSpace: CGRect
public let sourceNode: () -> (ASDisplayNode, CGRect)?
public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (ASDisplayNode, CGRect)?) {
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.sourceNode = sourceNode
}
}
public protocol ContextControllerContentSource: class {
var controller: ViewController { get }
var navigationController: NavigationController? { get }
@ -1548,6 +1740,7 @@ public protocol ContextControllerContentSource: class {
}
public enum ContextContentSource {
case reference(ContextReferenceContentSource)
case extracted(ContextExtractedContentSource)
case controller(ContextControllerContentSource)
}
@ -1576,6 +1769,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
public var dismissed: (() -> Void)?
private var shouldBeDismissedDisposable: Disposable?
@ -1590,23 +1784,29 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.displayTextSelectionTip = displayTextSelectionTip
super.init(navigationBarPresentationData: nil)
if case let .extracted(extractedSource) = source {
if !extractedSource.blurBackground {
switch source {
case .reference:
self.statusBar.statusBarStyle = .Ignore
}
self.shouldBeDismissedDisposable = (extractedSource.shouldBeDismissed
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
case let .extracted(extractedSource):
if extractedSource.blurBackground {
self.statusBar.statusBarStyle = .Hide
} else {
self.statusBar.statusBarStyle = .Ignore
}
strongSelf.dismiss(result: .default, completion: {})
})
} else {
self.statusBar.statusBarStyle = .Hide
self.shouldBeDismissedDisposable = (extractedSource.shouldBeDismissed
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.dismiss(result: .default, completion: {})
})
case .controller:
self.statusBar.statusBarStyle = .Hide
}
self.lockOrientation = true
self.blocksBackgroundWhenInOverlay = true
}
@ -1691,6 +1891,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
self.dismissed?()
}
}
@ -1705,6 +1906,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
self.dismissed?()
}
}
}

View File

@ -13,6 +13,8 @@ public final class ContextContentContainerNode: ASDisplayNode {
return
}
switch contentNode {
case .reference:
break
case .extracted:
break
case let .controller(controller):

View File

@ -1,6 +1,9 @@
import Foundation
import AsyncDisplayKit
public final class ContextReferenceContentNode: ASDisplayNode {
}
public final class ContextExtractedContentContainingNode: ASDisplayNode {
public let contentNode: ContextExtractedContentNode
public var contentRect: CGRect = CGRect()
@ -58,6 +61,7 @@ public final class ContextControllerContentNode: ASDisplayNode {
}
public enum ContextContentNode {
case reference(node: ContextReferenceContentNode)
case extracted(node: ContextExtractedContentContainingNode, keepInPlace: Bool)
case controller(ContextControllerContentNode)
}

View File

@ -257,7 +257,7 @@ private struct ChannelBannedMemberControllerState: Equatable {
var updating: Bool = false
}
private func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags {
func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags {
var result = flags
result.remove(.banReadMessages)
if result.contains(.banSendGifs) {

View File

@ -417,26 +417,6 @@ func groupPermissionDependencies(_ right: TelegramChatBannedRightsFlags) -> Tele
}
}
private func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags {
var result = flags
result.remove(.banReadMessages)
if result.contains(.banSendGifs) {
result.insert(.banSendStickers)
result.insert(.banSendGifs)
result.insert(.banSendGames)
} else {
result.remove(.banSendStickers)
result.remove(.banSendGifs)
result.remove(.banSendGames)
}
if result.contains(.banEmbedLinks) {
result.insert(.banSendInline)
} else {
result.remove(.banSendInline)
}
return result
}
private func channelPermissionsControllerEntries(context: AccountContext, presentationData: PresentationData, view: PeerView, state: ChannelPermissionsControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelPermissionsEntry] {
var entries: [ChannelPermissionsEntry] = []

View File

@ -33,7 +33,7 @@ public enum PeerReportOption {
case other
}
public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextController?, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], completion: @escaping (ReportReason?, Bool) -> Void) {
public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextController?, backAction: ((ContextController) -> Void)? = nil, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool = false, completion: @escaping (ReportReason?, Bool) -> Void) {
if let contextController = contextController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
@ -86,25 +86,29 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
}
let action: (String) -> Void = { message in
switch subject {
case let .peer(peerId):
let _ = (reportPeer(account: context.account, peerId: peerId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(reportReason, true)
})
case let .messages(messageIds):
let _ = (reportPeerMessages(account: context.account, messageIds: messageIds, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(reportReason, true)
})
case let .profilePhoto(peerId, photoId):
let _ = (reportPeerPhoto(account: context.account, peerId: peerId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(reportReason, true)
})
if passthrough {
completion(reportReason, true)
} else {
switch subject {
case let .peer(peerId):
let _ = (reportPeer(account: context.account, peerId: peerId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
case let .messages(messageIds):
let _ = (reportPeerMessages(account: context.account, messageIds: messageIds, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
case let .profilePhoto(peerId, photoId):
let _ = (reportPeerPhoto(account: context.account, peerId: peerId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
}
}
}
@ -133,11 +137,19 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
f(.dismissWithoutContent)
})))
}
if let backAction = backAction {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, action: { (c, _) in
backAction(c)
})))
}
contextController.setItems(.single(items))
} else {
contextController?.dismiss()
parent.view.endEditing(true)
parent.present(peerReportOptionsController(context: context, subject: subject, passthrough: false, present: { [weak parent] c, a in
parent.present(peerReportOptionsController(context: context, subject: subject, passthrough: passthrough, present: { [weak parent] c, a in
parent?.present(c, in: .window(.root), with: a)
}, push: { [weak parent] c in
parent?.push(c)
@ -200,37 +212,29 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
}
let action: (String) -> Void = { message in
switch subject {
case let .peer(peerId):
if passthrough {
completion(reportReason, true)
} else {
if passthrough {
completion(reportReason, true)
} else {
switch subject {
case let .peer(peerId):
let _ = (reportPeer(account: context.account, peerId: peerId, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
}
case let .messages(messageIds):
if passthrough {
completion(reportReason, true)
} else {
case let .messages(messageIds):
let _ = (reportPeerMessages(account: context.account, messageIds: messageIds, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
}
case let .profilePhoto(peerId, photoId):
if passthrough {
completion(reportReason, true)
} else {
case let .profilePhoto(peerId, photoId):
let _ = (reportPeerPhoto(account: context.account, peerId: peerId, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
}
}
}
}
@ -259,8 +263,6 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
} else {
action("")
}
} else {
push(peerReportController(context: context, subject: subject, completion: completion))
}
controller?.dismissAnimated()
@ -278,180 +280,3 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
])
return controller
}
private final class PeerReportControllerArguments {
let updateText: (String) -> Void
init(updateText: @escaping (String) -> Void) {
self.updateText = updateText
}
}
private enum PeerReportControllerSection: Int32 {
case text
}
private enum PeerReportControllerEntryTag: ItemListItemTag {
case text
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? PeerReportControllerEntryTag {
switch self {
case .text:
if case .text = other {
return true
} else {
return false
}
}
} else {
return false
}
}
}
private enum PeerReportControllerEntry: ItemListNodeEntry {
case text(PresentationTheme, String, String)
var section: ItemListSectionId {
switch self {
case .text:
return PeerReportControllerSection.text.rawValue
}
}
var stableId: Int32 {
switch self {
case .text:
return 0
}
}
static func ==(lhs: PeerReportControllerEntry, rhs: PeerReportControllerEntry) -> Bool {
switch lhs {
case let .text(lhsTheme, lhsText, lhsValue):
if case let .text(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
}
}
static func <(lhs: PeerReportControllerEntry, rhs: PeerReportControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PeerReportControllerArguments
switch self {
case let .text(theme, title, value):
return ItemListMultilineInputItem(presentationData: presentationData, text: value, placeholder: title, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { text in
arguments.updateText(text)
}, tag: PeerReportControllerEntryTag.text)
}
}
}
private struct PeerReportControllerState: Equatable {
var isReporting: Bool = false
var text: String = ""
}
private func peerReportControllerEntries(presentationData: PresentationData, state: PeerReportControllerState) -> [PeerReportControllerEntry] {
var entries: [PeerReportControllerEntry] = []
entries.append(.text(presentationData.theme, presentationData.strings.ReportPeer_ReasonOther_Placeholder, state.text))
return entries
}
private func peerReportController(context: AccountContext, subject: PeerReportSubject, completion: @escaping (ReportReason?, Bool) -> Void) -> ViewController {
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let statePromise = ValuePromise(PeerReportControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PeerReportControllerState())
let updateState: ((PeerReportControllerState) -> PeerReportControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let arguments = PeerReportControllerArguments(updateText: { text in
updateState { state in
var state = state
state.text = text
return state
}
})
let reportDisposable = MetaDisposable()
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightButton: ItemListNavigationButton
if state.isReporting {
rightButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
rightButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.text.isEmpty, action: {
var text: String = ""
updateState { state in
var state = state
if !state.isReporting && !state.text.isEmpty {
text = state.text
state.isReporting = true
}
return state
}
if !text.isEmpty {
let reportReason: ReportReason = .custom
let completed: () -> Void = {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.ReportPeer_AlertSuccess, actions: [TextAlertAction.init(type: TextAlertActionType.defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
completion(reportReason, true)
dismissImpl?()
}
switch subject {
case let .peer(peerId):
reportDisposable.set((reportPeer(account: context.account, peerId: peerId, reason: reportReason, message: text)
|> deliverOnMainQueue).start(completed: {
completed()
}))
case let .messages(messageIds):
reportDisposable.set((reportPeerMessages(account: context.account, messageIds: messageIds, reason: reportReason, message: text)
|> deliverOnMainQueue).start(completed: {
completed()
}))
case let .profilePhoto(peerId, photoId):
reportDisposable.set((reportPeerPhoto(account: context.account, peerId: peerId, reason: reportReason, message: text)
|> deliverOnMainQueue).start(completed: {
completed()
}))
}
}
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ReportPeer_ReasonOther_Title), leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
completion(nil, false)
}), rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: peerReportControllerEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: PeerReportControllerEntryTag.text)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
reportDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
return controller
}

View File

@ -230,7 +230,22 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
case .end:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallDeclineButton"), color: imageColor)
case .cancel:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallCancelButton"), color: imageColor)
image = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setLineWidth(4.0 - UIScreenPixel)
context.setLineCap(.round)
context.setStrokeColor(imageColor.cgColor)
context.move(to: CGPoint(x: 2.0 + UIScreenPixel, y: 2.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 26.0 - UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath()
context.move(to: CGPoint(x: 26.0 - UIScreenPixel, y: 2.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath()
})
}
if let image = image {

View File

@ -694,14 +694,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}) {
if let participantsContext = temporaryParticipantsContext.context.participantsContext {
let myPeerId = self.joinAsPeerId
let myPeer = self.accountContext.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(myPeerId)
let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in
if let peer = transaction.getPeer(myPeerId) {
return (peer, transaction.getPeerCachedData(peerId: myPeerId))
} else {
return nil
}
}
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
myPeer,
participantsContext.state,
participantsContext.activeSpeakers
).start(next: { [weak self] myPeer, state, activeSpeakers in
).start(next: { [weak self] myPeerAndCachedData, state, activeSpeakers in
guard let strongSelf = self else {
return
}
@ -721,7 +725,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
var participants = state.participants
if !participants.contains(where: { $0.peer.id == myPeerId }) {
if let myPeer = myPeer {
if let (myPeer, cachedData) = myPeerAndCachedData {
let about: String?
if let cachedData = cachedData as? CachedUserData {
about = cachedData.about
} else if let cachedData = cachedData as? CachedUserData {
about = cachedData.about
} else {
about = nil
}
participants.append(GroupCallParticipantsContext.Participant(
peer: myPeer,
ssrc: 0,
@ -731,7 +743,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
activityRank: nil,
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
volume: nil,
about: nil
about: about
))
participants.sort()
}

View File

@ -1438,6 +1438,7 @@ public final class VoiceChatController: ViewController {
}
let myPeerId = strongSelf.call.myPeerId
let darkTheme = strongSelf.darkTheme
var mainItemsImpl: (() -> Signal<[ContextMenuItem], NoError>)?
@ -1456,8 +1457,34 @@ public final class VoiceChatController: ViewController {
} else if let subscribers = peer.subscribers {
subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers)
}
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { _, f in
let isSelected = peer.peer.id == myPeerId
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|> map { image -> UIImage? in
if isSelected, let image = image {
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height))
let lineWidth = 1.0 + UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
})
} else {
return image
}
}
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in
f(.default)
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer.peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
})))
}
items.append(.separator)
@ -1537,8 +1564,12 @@ public final class VoiceChatController: ViewController {
}, action: { [weak self] _, f in
f(.default)
let controller = voiceChatTitleEditController(sharedContext: context.sharedContext, account: context.account, forceTheme: self?.darkTheme, title: self?.callState?.title, apply: { title in
self?.call.updateTitle(title ?? "")
let controller = voiceChatTitleEditController(sharedContext: context.sharedContext, account: context.account, forceTheme: self?.darkTheme, title: self?.callState?.title, apply: { [weak self] title in
if let strongSelf = self, let title = title {
strongSelf.call.updateTitle(title)
strongSelf.presentUndoOverlay(content: .succeed(text: strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))
})))
@ -1597,11 +1628,15 @@ public final class VoiceChatController: ViewController {
})))
if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp {
items.append(.custom(VoiceChatRecordingContextItem(timestamp: Double(recordingStartTimestamp), action: { _, f in
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in
f(.dismissWithoutContent)
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: {
self?.call.setShouldBeRecording(false)
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { [weak self] in
if let strongSelf = self {
strongSelf.call.setShouldBeRecording(false)
strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { _ in return false })
}
})])
self?.controller?.present(alertController, in: .window(.root))
}), false))
@ -1611,8 +1646,12 @@ public final class VoiceChatController: ViewController {
}, action: { _, f in
f(.dismissWithoutContent)
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_StartRecordingTitle, text: strongSelf.presentationData.strings.VoiceChat_StartRecordingText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StartRecordingStart, action: {
self?.call.setShouldBeRecording(true)
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_StartRecordingTitle, text: strongSelf.presentationData.strings.VoiceChat_StartRecordingText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StartRecordingStart, action: { [weak self] in
if let strongSelf = self {
strongSelf.call.setShouldBeRecording(true)
strongSelf.presentUndoOverlay(content: .recording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
}
})])
self?.controller?.present(alertController, in: .window(.root))
})))
@ -1654,7 +1693,7 @@ public final class VoiceChatController: ViewController {
}
let optionsButton: VoiceChatHeaderButton = !strongSelf.recButton.isHidden ? strongSelf.recButton : strongSelf.optionsButton
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: optionsButton.extractedContainerNode, keepInPlace: false, blurBackground: false)), items: mainItemsImpl?() ?? .single([]), reactionItems: [], gesture: gesture)
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: optionsButton.extractedContainerNode, keepInPlace: true, blurBackground: false)), items: mainItemsImpl?() ?? .single([]), reactionItems: [], gesture: gesture)
strongSelf.controller?.presentInGlobalOverlay(contextController)
}
@ -1668,7 +1707,7 @@ public final class VoiceChatController: ViewController {
|> deliverOnMainQueue).start(next: { [weak self] color in
if let strongSelf = self {
strongSelf.currentAudioButtonColor = color
strongSelf.updateButtons(transition: .immediate)
strongSelf.updateButtons(animated: true)
}
})
@ -2214,7 +2253,7 @@ public final class VoiceChatController: ViewController {
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: self.currentSubtitle, transition: transition)
}
private func updateButtons(transition: ContainedViewLayoutTransition) {
private func updateButtons(animated: Bool) {
let audioButtonAppearance: CallControllerButtonItemNode.Content.Appearance
if let color = self.currentAudioButtonColor {
audioButtonAppearance = .color(.custom(color.rgb, 1.0))
@ -2272,11 +2311,11 @@ public final class VoiceChatController: ViewController {
soundTitle = self.presentationData.strings.Call_Audio
}
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: soundTitle, transition: .animated(duration: 0.3, curve: .linear))
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: soundTitle, transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate)
let cameraButtonSize = CGSize(width: 40.0, height: 40.0)
self.cameraButtonNode.update(size: cameraButtonSize, content: CallControllerButtonItemNode.Content(appearance: CallControllerButtonItemNode.Content.Appearance.blurred(isFilled: false), image: .camera), text: " ", transition: .animated(duration: 0.3, curve: .linear))
self.cameraButtonNode.update(size: cameraButtonSize, content: CallControllerButtonItemNode.Content(appearance: CallControllerButtonItemNode.Content.Appearance.blurred(isFilled: false), image: .camera), text: " ", transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate)
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
}
@ -2409,7 +2448,7 @@ public final class VoiceChatController: ViewController {
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
}
self.updateButtons(transition: transition)
self.updateButtons(animated: !isFirstTime)
/*var currentVideoOrigin = CGPoint(x: 4.0, y: (layout.statusBarHeight ?? 0.0) + 4.0)
for (_, _, videoNode) in self.videoNodes {

View File

@ -21,10 +21,10 @@ func generateStartRecordingIcon(color: UIColor) -> UIImage? {
}
final class VoiceChatRecordingContextItem: ContextMenuCustomItem {
fileprivate let timestamp: Double
fileprivate let timestamp: Int32
fileprivate let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void
init(timestamp: Double, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) {
init(timestamp: Int32, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) {
self.timestamp = timestamp
self.action = action
}
@ -187,8 +187,8 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen
return
}
let currentTime = CFAbsoluteTimeGetCurrent()
let duration = currentTime - item.timestamp
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let duration = max(0, timestamp - item.timestamp)
let subtextFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
self.statusNode.attributedText = NSAttributedString(string: stringForDuration(Int32(duration)), font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
@ -218,7 +218,12 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen
let verticalSpacing: CGFloat = 2.0
let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height
return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
let hadLayout = self.validLayout != nil
self.validLayout = size
if !hadLayout {
self.updateTime(transition: .immediate)
}
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)

View File

@ -418,6 +418,7 @@ func voiceChatTitleEditController(sharedContext: SharedAccountContext, account:
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.inputFieldNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_question.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_menuvoicechat.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_blocked.pdf",
"filename" : "ic_left.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_ivcloselarge.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_ivclosesmall.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_right.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_ivcollapse.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_ivsettings.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_lt_safari.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_lt_check.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_lt_smallerfont.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_lt_biggerfont.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_lt_search.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -59,6 +59,21 @@ private class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
} else {
let touch = touches.first!
self.initialLocation = touch.location(in: self.view)
let touchesArray = Array(touches)
if touchesArray.count == 2, let firstTouch = touchesArray.first, let secondTouch = touchesArray.last {
let firstLocation = firstTouch.location(in: self.view)
let secondLocation = secondTouch.location(in: self.view)
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
let dx = v1.x - v2.x
let dy = v1.y - v2.y
return sqrt(dx * dx + dy * dy)
}
if distance(firstLocation, secondLocation) > 100 {
self.state = .failed
}
}
}
}

View File

@ -885,7 +885,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
})))
} else if message.id.peerId.isReplies {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuBlock, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Block"), color: theme.actionSheet.destructiveActionTextColor)
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { controller, f in
interfaceInteraction.blockMessageAuthor(message, controller)
})))

View File

@ -425,6 +425,7 @@ func chatTextLinkEditController(sharedContext: SharedAccountContext, account: Ac
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.inputFieldNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {

View File

@ -25,19 +25,22 @@ final class PeerInfoState {
let updatingAvatar: PeerInfoUpdatingAvatar?
let updatingBio: String?
let avatarUploadProgress: CGFloat?
let highlightedButton: PeerInfoHeaderButtonKey?
init(
isEditing: Bool,
selectedMessageIds: Set<MessageId>?,
updatingAvatar: PeerInfoUpdatingAvatar?,
updatingBio: String?,
avatarUploadProgress: CGFloat?
avatarUploadProgress: CGFloat?,
highlightedButton: PeerInfoHeaderButtonKey?
) {
self.isEditing = isEditing
self.selectedMessageIds = selectedMessageIds
self.updatingAvatar = updatingAvatar
self.updatingBio = updatingBio
self.avatarUploadProgress = avatarUploadProgress
self.highlightedButton = highlightedButton
}
func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
@ -46,7 +49,8 @@ final class PeerInfoState {
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton
)
}
@ -56,7 +60,8 @@ final class PeerInfoState {
selectedMessageIds: selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton
)
}
@ -66,7 +71,8 @@ final class PeerInfoState {
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton
)
}
@ -76,7 +82,8 @@ final class PeerInfoState {
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: updatingBio,
avatarUploadProgress: self.avatarUploadProgress
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton
)
}
@ -86,7 +93,19 @@ final class PeerInfoState {
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: avatarUploadProgress
avatarUploadProgress: avatarUploadProgress,
highlightedButton: self.highlightedButton
)
}
func withHighlightedButton(_ highlightedButton: PeerInfoHeaderButtonKey?) -> PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: highlightedButton
)
}
}

View File

@ -49,18 +49,22 @@ enum PeerInfoHeaderButtonIcon {
final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
let key: PeerInfoHeaderButtonKey
private let action: (PeerInfoHeaderButtonNode) -> Void
let containerNode: ASDisplayNode
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let backgroundNode: ASImageNode
private let textNode: ImmediateTextNode
private var theme: PresentationTheme?
private var icon: PeerInfoHeaderButtonIcon?
private var isActive: Bool?
init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode) -> Void) {
self.key = key
self.action = action
self.containerNode = ASDisplayNode()
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
@ -73,9 +77,10 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
self.accessibilityTraits = .button
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.backgroundNode)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.textNode)
self.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
@ -96,13 +101,14 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
self.action(self)
}
func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.theme != presentationData.theme || self.icon != icon {
func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.theme != presentationData.theme || self.icon != icon || self.isActive != isActive {
self.theme = presentationData.theme
self.icon = icon
self.isActive = isActive
self.backgroundNode.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor)
context.setFillColor(isActive ? presentationData.theme.list.itemAccentColor.cgColor : presentationData.theme.list.itemDisabledTextColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor)
@ -137,7 +143,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
})
}
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: isActive ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemDisabledTextColor)
self.accessibilityLabel = text
let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude))
@ -145,6 +151,8 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height + 6.0), size: titleSize))
transition.updateAlpha(node: self.textNode, alpha: isExpanded ? 0.0 : 1.0)
self.referenceNode.frame = self.containerNode.bounds
}
}
@ -2575,7 +2583,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let usernameNodeContainer: ASDisplayNode
let usernameNodeRawContainer: ASDisplayNode
let usernameNode: MultiScaleTextNode
private var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:]
var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:]
private let backgroundNode: ASDisplayNode
private let expandedBackgroundNode: ASDisplayNode
let separatorNode: ASDisplayNode
@ -3349,7 +3357,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
buttonText = presentationData.strings.PeerInfo_ButtonLeave
buttonIcon = .leave
}
buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isExpanded: self.isAvatarExpanded, presentationData: presentationData, transition: buttonTransition)
var isActive = true
if let highlightedButton = state.highlightedButton {
isActive = buttonKey == highlightedButton
}
buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isActive: isActive, isExpanded: self.isAvatarExpanded, presentationData: presentationData, transition: buttonTransition)
transition.updateSublayerTransformScaleAdditive(node: buttonNode, scale: buttonsScale)
if wasAdded {

View File

@ -1507,7 +1507,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
selectedMessageIds: nil,
updatingAvatar: nil,
updatingBio: nil,
avatarUploadProgress: nil
avatarUploadProgress: nil,
highlightedButton: nil
)
private let nearbyPeerDistance: Int32?
private var dataDisposable: Disposable?
@ -1597,7 +1598,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
self?.updateBlocked(block: block)
},
openReport: { [weak self] user in
self?.openReport(user: user)
self?.openReport(user: user, contextController: nil, backAction: nil)
},
openShareBot: { [weak self] in
self?.openShareBot()
@ -3403,191 +3404,268 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
guard let data = self.data, let peer = data.peer else {
return
}
let actionSheet = ActionSheetController(presentationData: self.presentationData)
let dismissAction: () -> Void = { [weak actionSheet] in
actionSheet?.dismissAnimated()
let presentationData = self.presentationData
self.state = self.state.withHighlightedButton(.more)
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
var items: [ActionSheetItem] = []
if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false).contains(.search) || (self.headerNode.isAvatarExpanded && self.peerId.namespace == Namespaces.Peer.CloudUser) {
items.append(ActionSheetButtonItem(title: presentationData.strings.ChatSearch_SearchPlaceholder, color: .accent, action: { [weak self] in
dismissAction()
self?.openChatWithMessageSearch()
}))
var mainItemsImpl: (() -> Signal<[ContextMenuItem], NoError>)?
let displayAsItems: () -> Signal<[ContextMenuItem], NoError> = {
return .single([])
}
if let user = peer as? TelegramUser {
if let botInfo = user.botInfo {
if botInfo.flags.contains(.worksWithGroups) {
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_InviteBotToGroup, color: .accent, action: { [weak self] in
dismissAction()
self?.openAddBotToGroup()
}))
}
if user.username != nil {
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_ShareBot, color: .accent, action: { [weak self] in
dismissAction()
self?.openShareBot()
}))
mainItemsImpl = {
var items: [ContextMenuItem] = []
let headerButtons = peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false)
let hasSearch = !headerButtons.contains(.search) || (self.headerNode.isAvatarExpanded && self.peerId.namespace == Namespaces.Peer.CloudUser)
if hasSearch {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatSearch_SearchPlaceholder, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openChatWithMessageSearch()
})))
}
if let user = peer as? TelegramUser {
if let botInfo = user.botInfo {
if botInfo.flags.contains(.worksWithGroups) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_InviteBotToGroup, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openAddBotToGroup()
})))
}
if user.username != nil {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ShareBot, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openShareBot()
})))
}
if let cachedData = data.cachedData as? CachedUserData, let botInfo = cachedData.botInfo {
for command in botInfo.commands {
if command.text == "settings" {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_BotSettings, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.performBotCommand(command: .settings)
})))
} else if command.text == "help" {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_BotHelp, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Help"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.performBotCommand(command: .help)
})))
} else if command.text == "privacy" {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_BotPrivacy, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.performBotCommand(command: .privacy)
})))
}
}
}
}
if let cachedData = data.cachedData as? CachedUserData, let botInfo = cachedData.botInfo {
for command in botInfo.commands {
if command.text == "settings" {
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotSettings, color: .accent, action: { [weak self] in
dismissAction()
self?.performBotCommand(command: .settings)
}))
} else if command.text == "help" {
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotHelp, color: .accent, action: { [weak self] in
dismissAction()
self?.performBotCommand(command: .help)
}))
} else if command.text == "privacy" {
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotPrivacy, color: .accent, action: { [weak self] in
dismissAction()
self?.performBotCommand(command: .privacy)
}))
if user.botInfo == nil && data.isContact {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_ShareContactButton, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
if let strongSelf = self, let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone {
let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)
let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact)))
strongSelf.controller?.present(shareController, in: .window(.root))
}
})))
}
if self.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_StartSecretChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openStartSecretChat()
})))
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)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.updateBlocked(block: true)
})))
}
}
}
}
if user.botInfo == nil && data.isContact {
items.append(ActionSheetButtonItem(title: presentationData.strings.Profile_ShareContactButton, color: .accent, action: { [weak self] in
dismissAction()
guard let strongSelf = self else {
return
}
if let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone {
let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)
let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact)))
strongSelf.controller?.present(shareController, in: .window(.root))
}
}))
}
if self.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) {
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in
dismissAction()
self?.openStartSecretChat()
}))
if data.isContact {
} else if self.peerId.namespace == Namespaces.Peer.SecretChat && data.isContact {
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
} else {
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_BlockUser, color: .destructive, action: { [weak self] in
dismissAction()
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_BlockUser, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.updateBlocked(block: true)
}))
})))
}
}
} else if self.peerId.namespace == Namespaces.Peer.SecretChat && data.isContact {
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
} else {
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_BlockUser, color: .destructive, action: { [weak self] in
dismissAction()
self?.updateBlocked(block: true)
}))
} else if let channel = peer as? TelegramChannel {
if !channel.flags.contains(.hasVoiceChat) {
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.requestCall(isVideo: false)
})))
}
}
}
} else if let channel = peer as? TelegramChannel {
if !channel.flags.contains(.hasVoiceChat) {
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_CreateVoiceChat, color: .accent, action: { [weak self] in
dismissAction()
self?.requestCall(isVideo: false)
}))
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openStats()
})))
}
var canReport = true
if channel.isVerified {
canReport = false
}
if channel.adminRights != nil {
canReport = false
}
}
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_Stats, color: .accent, action: { [weak self] in
dismissAction()
self?.openStats()
}))
}
var canReport = true
if channel.isVerified {
canReport = false
}
if channel.adminRights != nil {
canReport = false
}
if channel.flags.contains(.isCreator) {
canReport = false
}
if canReport {
items.append(ActionSheetButtonItem(title: presentationData.strings.ReportPeer_Report, color: .destructive, action: { [weak self] in
dismissAction()
self?.openReport(user: false)
}))
}
switch channel.info {
case .broadcast:
if channel.flags.contains(.isCreator) {
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: { [weak self] in
dismissAction()
self?.openDeletePeer()
}))
} else {
if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false).contains(.leave) {
canReport = false
}
if canReport {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ReportPeer_Report, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
self?.openReport(user: false, contextController: c, backAction: { c in
if let mainItemsImpl = mainItemsImpl {
c.setItems(mainItemsImpl())
}
})
})))
}
switch channel.info {
case .broadcast:
if channel.flags.contains(.isCreator) {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_DeleteChannel, textColor: .destructive, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openDeletePeer()
})))
} else {
if !headerButtons.contains(.leave) {
if case .member = channel.participationStatus {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Channel_LeaveChannel, textColor: .destructive, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openLeavePeer()
})))
}
}
}
case .group:
if channel.flags.contains(.isCreator) {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_DeleteGroup, textColor: .destructive, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openDeletePeer()
})))
} else {
if case .member = channel.participationStatus {
items.append(ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: { [weak self] in
dismissAction()
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Group_LeaveGroup, textColor: .destructive, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openLeavePeer()
}))
})))
}
}
}
case .group:
if channel.flags.contains(.isCreator) {
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteGroup, color: .destructive, action: { [weak self] in
dismissAction()
self?.openDeletePeer()
}))
} else {
if case .member = channel.participationStatus {
items.append(ActionSheetButtonItem(title: presentationData.strings.Group_LeaveGroup, color: .destructive, action: { [weak self] in
dismissAction()
self?.openLeavePeer()
}))
}
}
}
} else if let group = peer as? TelegramGroup {
var canManageGroupCalls = false
if case .creator = group.role {
canManageGroupCalls = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canManageCalls) {
} else if let group = peer as? TelegramGroup {
var canManageGroupCalls = false
if case .creator = group.role {
canManageGroupCalls = true
}
}
if canManageGroupCalls, !group.flags.contains(.hasVoiceChat) {
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_CreateVoiceChat, color: .accent, action: { [weak self] in
dismissAction()
guard let strongSelf = self else {
return
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canManageCalls) {
canManageGroupCalls = true
}
strongSelf.createAndJoinGroupCall(peerId: group.id)
}))
}
if canManageGroupCalls, !group.flags.contains(.hasVoiceChat) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.requestCall(isVideo: false)
})))
}
if case .Member = group.membership {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Group_LeaveGroup, textColor: .destructive, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openLeavePeer()
})))
}
}
if case .Member = group.membership {
items.append(ActionSheetButtonItem(title: presentationData.strings.Group_LeaveGroup, color: .destructive, action: { [weak self] in
dismissAction()
self?.openLeavePeer()
}))
}
return .single(items)
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.view.endEditing(true)
controller.present(actionSheet, in: .window(.root))
if let sourceNode = self.headerNode.buttonNodes[.more]?.referenceNode {
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoButtonReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: mainItemsImpl?() ?? .single([]), reactionItems: [], gesture: nil)
contextController.dismissed = { [weak self] in
if let strongSelf = self {
strongSelf.state = strongSelf.state.withHighlightedButton(nil)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
}
}
controller.presentInGlobalOverlay(contextController)
}
case .addMember:
self.openAddMember()
case .search:
@ -3623,14 +3701,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
private func openChatForReporting(_ reason: ReportReason) {
if let navigationController = (self.controller?.navigationController as? NavigationController) {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: .default, reportReason: reason, completion: { _ in
// var viewControllers = navigationController.viewControllers
// viewControllers = viewControllers.filter { controller in
// if controller is PeerInfoScreen {
// return false
// }
// return true
// }
// navigationController.setViewControllers(viewControllers, animated: false)
}))
}
}
@ -4114,7 +4184,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
controller.push(statsController)
}
private func openReport(user: Bool) {
private func openReport(user: Bool, contextController: ContextController?, backAction: ((ContextController) -> Void)?) {
guard let controller = self.controller else {
return
}
@ -4126,17 +4196,14 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} else {
options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other]
}
controller.present(peerReportOptionsController(context: self.context, subject: .peer(self.peerId), options: options, passthrough: true, present: { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}, push: { [weak controller] c in
controller?.push(c)
}, completion: { [weak self] reason, _ in
presentPeerReportOptions(context: self.context, parent: controller, contextController: contextController, backAction: backAction, subject: .peer(self.peerId), options: options, passthrough: true, completion: { [weak self] reason, _ in
if let reason = reason {
DispatchQueue.main.async {
self?.openChatForReporting(reason)
}
}
}), in: .window(.root))
})
}
private func openEncryptionKey() {
@ -6507,6 +6574,20 @@ private final class MessageContextExtractedContentSource: ContextExtractedConten
}
}
private final class PeerInfoButtonReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
func presentAddMembers(context: AccountContext, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) {
let members: Promise<[PeerId]> = Promise()
if groupPeer.id.namespace == Namespaces.Peer.CloudChannel {

View File

@ -32,6 +32,7 @@ public enum UndoOverlayContent {
case autoDelete(isOn: Bool, title: String?, text: String)
case gigagroupConversion(text: String)
case linkRevoked(text: String)
case recording(text: String)
}
public enum UndoOverlayAction {

View File

@ -549,6 +549,21 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 3
case let .recording(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_linkrevoked", colors: [:], scale: 0.066)
self.animatedStickerNode = nil
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 3
}
@ -579,7 +594,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content {
case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked:
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .recording:
break
case .dice:
self.panelWrapperNode.clipsToBounds = true